PageSourceSearch

https://simplyred.co.uk/

html simplyred.co.uk collected 2026-09-30 11:44:55 UTC 127,113 bytes, 2,357 lines download raw bytes

1<!doctype html>
2<html lang="en-GB">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width,initial-scale=1">
6<meta name="color-scheme" content="light">
7<title>Simply Red — Summer Funk ’27 Tour</title>
8<meta name="description" content="Simply Red — the Summer Funk ’27 tour across the UK and Europe. Sign up for first access to the artist pre-sale.">
9<meta name="theme-color" content="#F0F0EF">
10<meta name="format-detection" content="telephone=no">
11<meta name="author" content="Simply Red">
12<link rel="canonical" href="https://www.simplyred.com/">
13<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
14<link rel="icon" href="https://www.simplyred.com/favicon.ico">
15<link rel="apple-touch-icon" href="https://www.simplyred.com/stage/wp-content/uploads/favicon-320x320.png">
16
17<meta property="og:type" content="website">
18<meta property="og:site_name" content="SIMPLYRED.COM">
19<meta property="og:locale" content="en_GB">
20<meta property="og:title" content="Simply Red — Summer Funk ’27 Tour">
21<meta property="og:description" content="The Summer Funk ’27 tour across the UK and Europe. Sign up for first access to the artist pre-sale.">
22<meta property="og:url" content="https://www.simplyred.com/">
23<!-- 1200×630 composed from the hero assets (logo, script, key visual on the
24     ground colour). Swap for Dan's export if one comes; same filename. -->
25<meta property="og:image" content="https://www.simplyred.com/announcement-assets/og.jpg">
26<meta property="og:image:type" content="image/jpeg">
27<meta property="og:image:width" content="1200">
28<meta property="og:image:height" content="630">
29<meta property="og:image:alt" content="Simply Red — Summer Funk ’27">
30<meta name="twitter:card" content="summary_large_image">
31<meta name="twitter:title" content="Simply Red — Summer Funk ’27 Tour">
32<meta name="twitter:description" content="The Summer Funk ’27 tour across the UK and Europe. Sign up for first access to the artist pre-sale.">
33<meta name="twitter:image" content="https://www.simplyred.com/announcement-assets/og.jpg">
34
35<script type="application/ld+json">
36{
37  "@context": "https://schema.org",
38  "@type": "MusicGroup",
39  "name": "Simply Red",
40  "url": "https://www.simplyred.com/",
41  "sameAs": [
42    "https://www.instagram.com/simplyredofficial/",
43    "https://www.facebook.com/simplyred/",
44    "https://www.tiktok.com/@simplyredofficial",
45    "https://www.youtube.com/channel/UCwtvFWbWkLr8ra51W-pLstQ",
46    "https://open.spotify.com/artist/1fa0cOhromAZdq2xRA4vv8",
47    "https://music.apple.com/gb/artist/simply-red/163002",
48    "https://www.deezer.com/en/artist/1516"
49  ]
50}
51</script>
vendor: 1 bytes, line 51
51
52<script type="application/ld+json">
53{
54  "@context": "https://schema.org",
55  "@type": "ConcertSeries",
56  "name": "Simply Red — Summer Funk ’27 Tour",
57  "description": "Simply Red’s Summer Funk ’27 tour across the UK and Europe.",
58  "performer": { "@type": "MusicGroup", "name": "Simply Red" },
59  "url": "https://www.simplyred.com/"
60}
61</script>
61
62
63<script>
64  document.documentElement.classList.add("js");
65  /* GTM — both containers the current WP site loads, only after cookie
66     consent (see the cookie banner script at the foot of the page).
67     GA4 property G-Q3MK41W88D is fired BY GTM-5QVH3P6J (verified in the
68     container JS 2026-09-15), alongside Meta, Google Ads and Clarity tags.
69     Do NOT add a direct gtag() for G-Q3MK41W88D here — it would double-count
70     page views and enhanced-measurement events. Custom events go through
71     SR_track() → dataLayer, named sr_*; GTM needs a Custom Event trigger per
72     name to forward them to GA4 (list in docs/landing-page-plan.md). */
73  window.SR_GTM_IDS = ["GTM-TNHFM53", "GTM-5QVH3P6J"];
74  window.SR_loadGTM = function () {
75    if (window.SR_gtmLoaded) return;
76    window.SR_gtmLoaded = true;
77    window.dataLayer = window.dataLayer || [];
78    window.dataLayer.push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
79    window.SR_GTM_IDS.forEach(function (id) {
80      var s = document.createElement("script");
81      s.async = true;
82      s.src = "https://www.googletagmanager.com/gtm.js?id=" + id;
83      document.head.appendChild(s);
84    });
85  };
86  window.SR_track = function (name, params) {
87    window.dataLayer = window.dataLayer || [];
88    var p = { event: name }; for (var k in (params || {})) p[k] = params[k];
89    window.dataLayer.push(p);
90  };
91  try {
92    if (localStorage.getItem("sr_cookie_consent") === "1") window.SR_loadGTM();
93  } catch (e) {}
94</script>
94
95
96<!-- Fonts. Two Adobe kits:
97       fjk5ujc — the current simplyred.com theme's kit: forma-djr-text
98                 (300–800) + owners-wide (500/700)
99       dkt5nqg — Dharma Gothic C/E/M. M ships Regular / Bold / ExBold /
100                 Heavy (400/700/800/900) — the weights the Figma uses.
101     Saira Extra Condensed / Archivo remain as network fallbacks only. -->
102<link rel="preconnect" href="https://use.typekit.net" crossorigin>
103<link rel="stylesheet" href="https://use.typekit.net/fjk5ujc.css">
104<link rel="stylesheet" href="https://use.typekit.net/dkt5nqg.css">
105<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
106<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
107<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;700&family=Saira+Extra+Condensed:wght@400;700;800&display=swap">
108
109<style>
110  /* ============================================================
111     Tokens — from Dan's hero prototype + the Figma landing frame
112     (RjNy0ZlZGRbV74d5Zusjvs, node 31:414). Light ground, red accent.
113     ============================================================ */
114  :root {
115    --ground: #F0F0EF;
116    --ink: #1D1D1B;
117    --white: #FFFFFF;
118    --accent: #D03131;
119    --accent-dark: #B82B2B;
120    /* Small red text on the light ground. #D03131 is 4.42:1 there (AA needs
121       4.5); this is 4.61:1 and indistinguishable. Display type + buttons
122       keep the true brand red (large text 3:1 / white-on-red 5:1 both pass). */
123    --accent-text: #CC2E2E;
124    --text-2: #404042;
125    --text-3: #666668;        /* was #6E6E70 — 4.46:1 on the ground, now 5.0:1 */
126    --icon-muted: #9A9A9C;
127    --hairline: #DCDBD9;
128    --footer: #10100F;
129    --panel: #1D1D1B;
130
131    --sans: "forma-djr-text", "Forma DJR Text", Archivo, system-ui, -apple-system, "Segoe UI", sans-serif;
132    --display: "dharma-gothic-m", "Dharma Gothic M", "Saira Extra Condensed", Impact, sans-serif;
133    --wide: "owners-wide", "Owners Wide", Archivo, system-ui, sans-serif;
134
135    /* Large-desktop scale. Type, buttons, row heights and containers all
136       multiply by --k so a 1920+ display doesn't feel like a 1440 layout
137       floating in space. The hero has its own stage scale. */
138    --k: 1;
139    --kd: calc(1 + (var(--k) - 1) * 0.5);   /* date rows: half the boost, they get loud fast */
140    --container: min(calc(960px * var(--k)), calc(100vw - 32px));
141    --wide-container: min(calc(1200px * var(--k)), calc(100vw - 32px));
142
143    /* Hero stage — --s and --stageBase set by JS. The sticky hero is at
144       least one viewport tall so the first view is the hero alone; the
145       1440×852 stage sits vertically centred inside that. */
146    --s: 1;
147    --navS: 1;           /* nav pill scale — tracks --s but never above 1 */
148    --stageBase: 852px;
149    --stageH: max(var(--stageBase), 100svh);
150  }
151
152  @media (min-width: 1680px) { :root { --k: 1.2; } }
153  @media (min-width: 2200px) { :root { --k: 1.4; } }
154
155  *, *::before, *::after { box-sizing: border-box; }
156  /* overflow-x: clip, not hidden — hidden would make body a scroll container
157     and break the sticky hero. clip just stops sideways scroll. */
158  html, body { margin: 0; padding: 0; overflow-x: clip; }
159  html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
160  body {
161    background: var(--ground);
162    color: var(--ink);
163    font-family: var(--sans);
164    font-size: calc(16px * var(--k));
165    line-height: 1.4;
166    letter-spacing: 0.02em;
167    -webkit-font-smoothing: antialiased;
168    -moz-osx-font-smoothing: grayscale;
169    min-height: 100vh;
170  }
171  body.no-scroll { overflow: hidden; }
172  a { color: inherit; text-decoration: none; }
173  /* Every link and button eases between states. Components with richer
174     hover behaviour (buttons, rows, tiles) set their own transition lists. */
175  a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
176  a, button {
177    transition: color 180ms ease, background-color 180ms ease, border-color 180ms ease,
178                opacity 180ms ease, transform 180ms ease, box-shadow 180ms ease;
179  }
180  img, svg { display: block; max-width: 100%; }
181  [hidden] { display: none !important; }
182  p { margin: 0; }
183
184  .vh {
185    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
186    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
187  }
188  .skip-link {
189    position: absolute; left: -9999px; top: 0; z-index: 200;
190    background: var(--ink); color: var(--white); padding: 8px 12px; font-size: calc(14px * var(--k));
191  }
192  .skip-link:focus { left: 8px; top: 8px; }
193
194  /* Display type — Dharma Gothic M. ExBold (800) for headings and dates,
195     Heavy (900) for city + region labels, Regular (400) for venue names. */
196  .display, h1, h2, h3 {
197    font-family: var(--display);
198    font-weight: 800;
199    line-height: 1.1;
200    letter-spacing: 0.04em;
201    text-transform: uppercase;
202    margin: 0;
203  }
204  .display em, h2 em { font-style: normal;
204 color: var(--accent); }
205
206  /* State-driven copy — every element carrying data-copy-* attributes gets
207     its text swapped by JS according to the page state (announce → presale
208     → onsale). The HTML holds the announce copy as the no-JS default. */
209
210  /* ============================================================
211     Buttons — SM · Button component: red fill, 2px radius, 16/500.
212     ============================================================ */
213  .btn {
214    appearance: none;
215    position: relative;
216    display: inline-flex;
217    align-items: center;
218    justify-content: center;
219    min-height: calc(54px * var(--k));
220    padding: calc(16px * var(--k)) calc(24px * var(--k));
221    border: 0;
222    border-radius: 2px;
223    background: var(--accent);
224    color: var(--white);
225    font-family: var(--sans);
226    font-weight: 500;
227    font-size: calc(16px * var(--k));
228    line-height: 1.4;
229    letter-spacing: 0.02em;
230    cursor: pointer;
231    text-align: center;
232    text-decoration: none;
233    white-space: nowrap;
234    transition: background-color 160ms ease, color 160ms ease, opacity 160ms ease;
235  }
236  .btn:hover, .btn:focus-visible { background: var(--accent-dark); }
237  .btn:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
238  .btn--outline {
239    background: transparent;
240    color: var(--accent);
241    box-shadow: inset 0 0 0 1px var(--accent);
242  }
243  .btn--outline:hover, .btn--outline:focus-visible { background: var(--accent); color: var(--white); }
244  .btn--block { width: 100%; }
245
246  .text-link {
247    font-weight: 500;
248    color: var(--accent-text);
249    transition: color 160ms ease;
250  }
251  .text-link:hover, .text-link:focus-visible { color: var(--accent-dark); }
252
253  .eyebrow-pill {
254    display: inline-flex;
255    align-items: center;
256    padding: 6px 14px;
257    border: 1px solid var(--accent-text);
258    border-radius: 999px;
259    font-family: var(--sans);
260    font-weight: 500;
261    font-size: calc(11px * var(--k));
262    letter-spacing: 0.02em;
263    text-transform: uppercase;
264    color: var(--accent-text);
265    margin: 0 0 21px;
266  }
267
268  /* ============================================================
269     Section shells
270     ============================================================ */
271  section { position: relative; scroll-margin-top: 96px; }
272  .section-inner {
273    width: var(--container);
274    margin: 0 auto;
275    padding: clamp(56px, 8vw, 112px) 0;
276    text-align: center;
277  }
278  .section-heading {
279    font-size: clamp(32px, 4vw, calc(40px * var(--k)));
280    margin: 0 0 8px;
281  }
282  .section-cta { margin: 24px 0 0; }
283  .section-lede {
284    font-size: calc(16px * var(--k));
285    line-height: 1.5;
286    color: var(--text-2);
287    max-width: 60ch;
288    margin: 0 auto;
289  }
290
291  /* ============================================================
292     NAV — frosted pill, fixed to the viewport. Lifted out of the
293     scaled hero stage so it sticks; keeps the stage scale + 41px
294     design offset via --s. Tour / Music / simplyred.com →
295     ============================================================ */
296  .navbar {
297    position: fixed; top: 0; left: 0; right: 0; z-index: 60;
298    display: flex; justify-content: center;
299    padding-top: calc(41px * var(--navS) + env(safe-area-inset-top));
300    pointer-events: none;
301  }
302  .nav {
303    height: 68px; pointer-events: auto;
304    transform: scale(var(--navS)); transform-origin: top center;
305    display: flex; align-items: center; gap: 4px; padding: 8px;
306    border-radius: 999px;
307    background: var(--white);
308  }
309  .nav a {
310    display: flex; align-items: center; gap: 8px; height: 52px; padding: 0 28px;
311    border-radius: 999px; text-decoration: none;
312    font-size: 16px; line-height: 20px; letter-spacing: 0; font-weight: 500;
313    color: var(--ink); white-space: nowrap;
314    transition: background-color 180ms ease, color 180ms ease;
315  }
316  .nav a:hover { background: var(--ground); }
317  .nav a.is-active { background: var(--ground); font-weight: 700; }
318  .nav a.is-out { color: var(--accent-text); }
319  .nav a.is-out span { display: inline-block; transition: transform 180ms ease; }
320  .nav a.is-out:hover span { transform: translateX(3px); }
321  .nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
322  /* Small screens: the pill no longer tracks the stage scale, it gets its
323     own compact size so it stays tappable. */
324  @media (max-width: 719px) {
325    .navbar { padding-top: calc(12px + env(safe-area-inset-top)); }
326    .nav { transform: none; height: auto; padding: 4px; gap: 0; }
327    .nav a { height: 40px; padding: 0 14px; font-size: 14px; }
328  }
329
330  /* ============================================================
331     HERO — Dan's prototype, verbatim where possible. A 1440×852
332     stage scaled to fill the viewport. The page scrolls normally
333     from the top; the bands straighten over the first ~45% of a
334     screen of scroll while the hero is still in view (Dan's
335     prototype pinned the hero for this — dropped on review, it
336     felt like the page was stuck).
337     Geometry from Figma nodes 47:2239 / 47:2253.
338     ============================================================ */
339  .hero-scroll { position: relative; height: var(--stageH); }
340  .hero-sticky { position: relative; height: var(--stageH); overflow: hidden; }
341  .stage {
342    position: absolute; left: 50%; margin-left: -720px;
343    top: max(0px, calc((var(--stageH) - var(--stageBase)) / 2));
344    width: 1440px; height: 852px;
345    transform: scale(var(--s)); transform-origin: 50% 0;
346  }
347  .logo   { position: absolute; left: 416px; top: 162px; width: 608.618px; height: 66.44px; }
348  .script { position: absolute; left: 502px; top: 249px; width: 436.825px; height: 82.21px; }
349  .logo img, .script img { display: block; width: 100%; height: 100%; }
350
351  /* left-to-right reveal with a soft edge */
352  .wipe {
353    -webkit-mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
354            mask-image: linear-gradient(90deg, #000 45%, transparent 55%);
355    -webkit-mask-size: 220% 100%;   mask-size: 220% 100%;
356    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
357    -webkit-mask-position: 100% 0;  mask-position: 100% 0;
358  }
359
360  .kv { position: absolute; left: 467px; top: 367px; width: 502px; height: 452px; }
361  .band { position: absolute; will-change: transform, opacity; }
362  .band-inner { width: 100%; height: 100%; will-change: transform; }
363  .band img { display: block; width: 100%; height: 100%; }
364
365  .b-orange { left: 0;     top: 14px;   width: 83.6px; height: 377.7px; }
366  .b-blue   { left: 93px;  top: 25px;   width: 73px;   height: 375.9px; }
367  .b-green  { left: 173px; top: 0;      width: 72.5px; height: 380.1px; }
368  .b-red    { left: 251px; top: 46px;   width: 82.7px; height: 343.7px; }
369  .b-yellow { left: 328px; top: 0;      width: 83.5px; height: 377.7px; }
370  .b-purple { left: 417px; top: 23.3px; width: 85.2px; height: 378px; }
371  .b-mick   { left: 69px;  top: 115px;  width: 328px;  height: 336.8px; }
372
373  @keyframes riseIn { from { opacity: 0; transform: translateY(var(--rise, 20px)); } to { opacity: 1; transform: none; } }
374  @keyframes wipeIn { to { -webkit-mask-position: 0% 0; mask-position: 0% 0; } }
375  @keyframes cueIn  { from { opacity: 0; } to { opacity: 1; } }
376
377  .intro .navbar,
378  .intro .band { opacity: 0; animation: riseIn .9s cubic-bezier(.22, 1, .36, 1) forwards; }
379  .intro .navbar   { --rise: -10px; animation-duration: .7s; animation-delay: .15s; }
380  .intro .b-green  { animation-delay: .60s; }
381  .intro .b-red    { animation-delay: .66s; }
382  .intro .b-blue   { animation-delay: .72s; }
383  .intro .b-yellow { animation-delay: .78s; }
384  .intro .b-orange { animation-delay: .84s; }
385  .intro .b-purple { animation-delay: .90s; }
386  .intro .b-mick   { animation-delay: 1.30s; --rise: 26px; }
387  .intro .wipe { animation: wipeIn 1.05s cubic-bezier(.42, 0, .16, 1) .35s forwards; }
388  .intro .script .wipe { animation-duration: 1.35s; animation-delay: 1.15s; }
389  .intro .cue { opacity: 0; animation: cueIn .8s ease 2.3s forwards; }
390  /* Once the fade-in has run, hand opacity back to the scroll script — a
391     forwards-filled animation would otherwise pin it at 1. */
392  .intro .cue.is-live { animation: none; opacity: 1; }
393
394  /* Scroll mark (Figma 47:1876), centred over the figure. */
395  .cue {
396    position: absolute; left: 692px; top: 738px; width: 52px;
397    display: flex; flex-direction: column; align-items: center; gap: 6px;
398    text-decoration: none;
399  }
400  .cue span { font-size: 16px; line-height: 22px; letter-spacing: .02em; font-weight: 500; color: #fff; }
401  .cue svg { display: block; color: var(--accent); }
402  /* Mobile composition (< 720px): the 1440 stage is not scaled as a whole.
403     Logo + script sit under the nav, sized to the width; the key visual is
404     scaled on its own (--kvS, set by JS) and anchored to the bottom of the
405     screen so Mick is big and the strips fill the height. JS sets --logoTop,
406     --logoW, --scriptTop, --scriptW. */
407  @media (max-width: 719px) {
408    /* Height is fixed at load by JS (--stageBase) — no viewport units, so
409       the collapsing browser chrome can't re-flow the hero mid-scroll. */
410    :root { --stageH: var(--stageBase); }
411    .stage { width: 100%; left: 0; margin-left: 0; height: var(--stageH); transform: none; }
412    .logo, .script { left: 50%; transform: translateX(-50%); height: auto; }
413    .logo   { top: var(--logoTop, 96px);   width: var(--logoW, 86vw); }
414    .script { top: var(--scriptTop, 150px); width: var(--scriptW, 62vw); }
415    .logo img, .script img { height: auto; }
416    .kv { left: 50%; top: auto; bottom: -28px; transform-origin: 50% 100%; }
417    /* Outer strips tuck behind their neighbours (see MOBILE_REST in the JS). */
418    .b-orange, .b-purple { z-index: 1; }
419    .b-blue, .b-yellow { z-index: 2; }
420    .b-green, .b-red { z-index: 3; }
421    .b-mick { z-index: 4; }
422    .cue { left: 50%; top: auto; bottom: calc(24px + env(safe-area-inset-bottom)); transform: translateX(-50%); }
423  }
424  .cue:hover span { opacity: 0.8; }
425  .cue:hover svg { transform: translateY(2px); }
426  .cue span, .cue svg { transition: opacity 180ms ease, transform 180ms ease; }
427  .cue:focus-visible { outline: 2px solid #fff; outline-offset: 6px; border-radius: 4px; }
428
429  /* ============================================================
430     SIGN-UP — "First to know". Heading + lede from Figma 31:581,
431     then the OpenStage embed in place of the drawn form (OpenStage
432     renders its own fields, consent + reCAPTCHA inside the iframe;
433     it cannot be styled from here — see docs). Then the share row.
434     ============================================================ */
435  .signup { background: var(--ground); }
436  .signup-inner {
437    width: min(calc(492px * var(--k)), calc(100vw - 32px));
438    margin: 0 auto;
439    padding: clamp(40px, 6vw, 72px) 0 clamp(56px, 8vw, 96px);
440    text-align: center;
441  }
442  .signup h2 {
443    font-size: clamp(48px, 7vw, calc(64px * var(--k)));
444    line-height: 1.1;
445    min-height: 1.1em;
446  }
447  .signup .lede {
448    margin: 12px 0 0;
449    font-size: calc(18px * var(--k)); line-height: 1.5; color: var(--text-2);
450  }
451  .os-embed {
452    position: relative;
453    margin: 24px auto 0;
454    width: 100%;
455    max-width: calc(500px * var(--k));
456    /* OpenStage's embed says 410px, but its tallest step (consent) ends
457       around 300px; 340 removes the dead space under the form. Re-measure
458       if they add fields. */
459    min-height: 340px;
460    background: var(--white);
461    border-radius: 2px;
462    overflow: hidden;
463  }
464  .os-embed iframe {
465    display: block;
466    width: 100%;
467    height: 340px;
468    border: 0;
469    max-width: 100%;
470  }
471  .os-embed noscript p { padding: 24px; font-size: calc(14px * var(--k)); color: var(--text-3); }
472  .micro { margin: 18px 0 0; font-size: calc(12px * var(--k)); line-height: calc(18px * var(--k)); color: var(--text-3); }
473  .share-label { margin: 32px 0 0; font-size: calc(12px * var(--k)); line-height: calc(18px * var(--k)); color: var(--text-3); }
474  .share { margin-top: 16px; display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
475  .share a {
476    display: inline-flex; align-items: center; height: 36px; padding: 8px 14px;
477    border: 1px solid var(--accent-text); border-radius: 999px; text-decoration: none;
478    font-size: calc(16px * var(--k)); line-height: calc(20px * var(--k)); font-weight: 500; color: var(--accent-text);
479    transition: background .18s ease, color .18s ease;
480  }
481  .share a:hover, .share a:focus-visible { background: var(--accent); color: #fff; }
482  .share a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
483
484  /* Fade-up per component. Hidden state only applied once JS arms it. */
485  .armed .r {
486    opacity: 0; transform: translateY(18px);
487    transition: opacity .7s cubic-bezier(.22, 1, .36, 1) var(--d, 0ms),
488                transform .7s cubic-bezier(.22, 1, .36, 1) var(--d, 0ms);
489  }
490  .armed.in .r { opacity: 1; transform: none; }
491
492  /* Whole-section reveals for everything below the sign-up. */
493  .js .reveal {
494    opacity: 0;
495    transform: translateY(30px);
496    transition: opacity 800ms ease-out, transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
497    will-change: opacity, transform;
498  }
499  .js .reveal.is-visible { opacity: 1; transform: none; }
500
501  /* ============================================================
502     TOUR — heading, state-driven lede, full-bleed trailer, then
503     the dates grouped by region.
504     ============================================================ */
505  .tour .section-inner { padding-top: 0; }
506  /* Locked 16:9. Full-bleed, but never taller than the viewport — on very
507     wide screens it narrows and centres rather than cropping the frame. */
508  .tour-video {
509    --tv: min(100vw, calc(100vh * 16 / 9));
510    position: relative;
511    width: var(--tv);
512    margin: 24px 0 56px calc(50% - var(--tv) / 2);
513    aspect-ratio: 16 / 9;
514    background: var(--panel);
515    overflow: hidden;
516  }
517  .tour-video video,
518  .tour-video .tour-video-poster {
519    position: absolute; inset: 0;
520    width: 100%; height: 100%;
521    object-fit: cover;
522    display: block;
523  }
524  /* Logo + script sit over the poster until the trailer starts. */
525  .tour-video-overlay {
526    position: absolute; inset: 0;
527    display: flex; flex-direction: column; align-items: center; justify-content: center;
528    gap: clamp(8px, 1.5vw, 18px);
529    pointer-events: none;
530    transition: opacity 500ms ease;
531  }
532  .tour-video-overlay::before {          /* slight dim so the button reads */
533    content: ""; position: absolute; inset: 0; z-index: -1;
534    background: rgba(0, 0, 0, 0.18);
535  }
536  .tour-video-overlay img { width: min(501px, 55vw); height: auto; }
537  .tour-video-overlay img + img { width: min(360px, 40vw); }
538  .tour-video-overlay .btn { pointer-events: auto; margin-top: clamp(12px, 3vw, 32px); }
539  .tour-video.is-playing .tour-video-overlay { opacity: 0; }
540  .tour-video.is-playing .tour-video-overlay .btn { pointer-events: none; }
541  .tour-video.is-playing.is-paused .tour-video-overlay { opacity: 1; }
542  .tour-video.is-playing.is-paused .tour-video-overlay .btn { pointer-events: auto; }
543  .video-mute {
544    position: absolute;
545    right: clamp(12px, 2.5vw, 28px);
546    bottom: clamp(12px, 2.5vw, 28px);
547    z-index: 2;
548    display: inline-flex; align-items: center; justify-content: center;
549    width: 47px; height: 47px; padding: 0; border: 0; border-radius: 50%;
550    background: rgba(29, 29, 27, 0.75);
551    color: var(--white);
552    cursor: pointer;
553    transition: background-color 160ms ease, transform 160ms ease;
554  }
555  .video-mute:hover, .video-mute:focus-visible { background: rgba(29, 29, 27, 0.95); transform: scale(1.06); }
556  .video-mute:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
557  .video-mute svg { width: 20px; height: 20px; display: block; fill: currentColor; }
558  .video-mute[aria-pressed="false"] .icon-unmuted,
559  .video-mute[aria-pressed="true"]  .icon-muted { display: none; }
560
561  .dates-list { max-width: calc(960px * var(--k)); margin: 0 auto; text-align: left; }
562  .region-divider {
563    font-family: var(--display);
564    font-weight: 900;
565    font-size: clamp(32px, 4vw, calc(40px * var(--kd)));
566    line-height: calc(32px * var(--kd));
567    letter-spacing: 0.04em;
568    text-transform: uppercase;
569    color: var(--ink);
570    padding: 0 0 22px;
571    margin: 0;
572    border-bottom: 1px solid var(--text-2);
573  }
574  .region-divider + .region-divider,
575  .date-row + .region-divider,
576  .dates-empty + .region-divider { margin-top: 56px; }
577  .date-row {
578    display: grid;
579    grid-template-columns: calc(138px * var(--kd)) minmax(0, 1fr) auto;
580    gap: 24px;
581    align-items: center;
582    padding: calc(16px * var(--kd)) 0;
583    border-bottom: 1px solid var(--text-2);
584    color: inherit;
585    text-decoration: none;
586    transition: background-color 180ms ease;
587  }
588  .date-row[data-state="onsale"],
589  .date-row[data-state="presale"] { cursor: pointer; }
590  .date-row[data-state="onsale"]:hover,
591  .date-row[data-state="onsale"]:focus-visible,
592  .date-row[data-state="presale"]:hover,
593  .date-row[data-state="presale"]:focus-visible {
594    background: rgba(208, 49, 49, 0.06);
595    box-shadow: -12px 0 0 rgba(208, 49, 49, 0.06), 12px 0 0 rgba(208, 49, 49, 0.06);
596    outline: none;
597  }
598  .date-row:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
599  .date-when {
600    font-family: var(--display);
601    font-weight: 800;
602    font-size: calc(32px * var(--kd));
603    line-height: calc(32px * var(--kd));
604    letter-spacing: 0.04em;
605    text-transform: uppercase;
606    color: var(--accent);
607    white-space: nowrap;
608  }
609  .date-where {
610    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 10px;
611    font-family: var(--display);
612    font-size: calc(32px * var(--kd));
613    line-height: calc(32px * var(--kd));
614    letter-spacing: 0.04em;
615    text-transform: uppercase;
616    min-width: 0;
617  }
618  .date-city  { font-weight: 900; color: var(--ink); }
619  .date-venue { font-weight: 400; color: var(--text-2); }
620  .date-support {
621    flex-basis: 100%;
622    font-family: var(--sans);
623    font-size: calc(14px * var(--k)); line-height: calc(20px * var(--k));
624    letter-spacing: 0.02em; text-transform: none;
625    color: var(--text-3);
626    margin-top: 4px;
627  }
628  .date-cta {
629    display: inline-flex; align-items: center; justify-content: center;
630    min-height: calc(54px * var(--kd)); padding: calc(16px * var(--kd)) calc(24px * var(--kd));
631    border-radius: 2px;
632    background: var(--accent); color: var(--white);
633    font-family: var(--sans); font-weight: 500; font-size: calc(16px * var(--kd)); line-height: 1.4; letter-spacing: 0.02em;
634    white-space: nowrap;
635    transition: background-color 160ms ease;
636  }
637  .date-row:hover .date-cta { background: var(--accent-dark); }
638  /* Inert states — sold out + not-yet-on-sale read as grey. */
639  .date-row[data-state="soldout"] .date-cta,
640  .date-row[data-state="coming"] .date-cta {
641    background: var(--hairline); color: var(--text-3);
642  }
643  .date-row[data-state="soldout"] { cursor: not-allowed; }
644  .date-row[data-state="soldout"] .date-when { color: var(--text-3); }
645  .date-row[data-state="soldout"] .date-city { color: var(--text-2); }
646  .date-row[data-state="coming"] { cursor: default; }
647
648  .dates-empty {
649    padding: 32px 0;
650    font-size: calc(18px * var(--k)); line-height: 1.4; color: var(--text-2);
651    border-bottom: 1px solid var(--text-2);
652  }
653
654  /* "See all dates" pushdown — rows past the first five collapse. */
655  .dates-list .is-hidden {
656    max-height: 0; padding-block: 0; border-bottom: 0;
657    opacity: 0; transform: translateY(-6px); overflow: hidden; pointer-events: none;
658    transition: max-height 320ms ease, padding 320ms ease, opacity 260ms ease-out,
659                transform 320ms cubic-bezier(0.22, 1, 0.36, 1), margin 320ms ease;
660  }
661  .dates-list .region-divider.is-hidden { margin-top: 0; }
662  .dates-list.is-expanded .is-hidden {
663    max-height: 240px; padding-block: 16px; border-bottom: 1px solid var(--text-2);
664    opacity: 1; transform: none; pointer-events: auto;
665    transition-delay: calc(var(--i, 0) * 45ms);
666  }
667  .dates-list.is-expanded .region-divider.is-hidden { padding-block: 0 22px; margin-top: 56px; }
668  .dates-toggle { margin: 56px auto 0; min-width: 266px; }
669  .dates-toggle[hidden] { display: none; }
670
671  @media (max-width: 700px) {
672    .date-row { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; }
673    .date-when { grid-column: 1; font-size: calc(24px * var(--k)); line-height: calc(26px * var(--k)); }
674    .date-where { grid-column: 1; font-size: calc(24px * var(--k)); line-height: calc(26px * var(--k)); gap: 0 8px; }
675    .date-cta { grid-column: 2; grid-row: 1 / 3; align-self: center; min-height: 48px; padding: 12px 18px; font-size: calc(15px * var(--k)); }
676    .date-support { font-size: calc(13px * var(--k)); line-height: calc(18px * var(--k)); }
677    .region-divider { font-size: calc(28px * var(--k)); padding-bottom: 14px; }
678    .dates-list.is-expanded .is-hidden { max-height: 320px; }
679  }
680
681  /* ============================================================
682     PULL QUOTES — Dharma quote marks 140px, red opening, ink
683     closing (rotated), lead phrase in red. Figma 31:556 / 31:562.
684     ============================================================ */
685  .pull-quote .section-inner { width: min(1080px, calc(100vw - 32px)); max-width: none; padding-top: clamp(56px, 8vw, 128px); padding-bottom: clamp(56px, 8vw, 128px); }
686  /* The quote marks live inside the blockquote's side padding: their size is
687     derived from it and they're offset by exactly it, so they stay on screen
688     at every width (they were hanging off the edge below ~1500px). */
689  .pull-quote blockquote { --qpad: clamp(28px, 7vw, 110px); position: relative; margin: 0; padding: 0 var(--qpad); }
690  /* Zero-width inline anchors at the start of the first line and the end of
691     the last line; the marks hang off those, so they hug the text wherever
692     it wraps and can't leave the container (the gutter is their headroom).
693     Needs the split JS below — the no-JS fallback keeps the <em> lead. */
694  .pull-quote .qa { position: relative; display: inline-block; width: 0; height: 1em; vertical-align: top; }
695  /* No-JS fallback: the static marks sit at the paragraph's top-left and
696     bottom-right rather than hanging off the (JS-made) anchors. */
697  .pull-quote blockquote:not(.is-split) .quote-mark--open  { right: auto; left: calc(-1 * var(--qpad)); }
698  .pull-quote blockquote:not(.is-split) .quote-mark--close { left: auto; right: calc(-1 * var(--qpad)); top: auto; bottom: -0.5em; }
699  .pull-quote blockquote p {
700    position: relative;
701    font-family: var(--display);
702    font-weight: 800;
703    /* Tracks the viewport so the quote fills the window with intent:
704       ~36px at 1440, ~48px at 1920, capped at 60px. (Dharma is condensed —
705       the column is held at 1080px so it sets on ~3 lines, per the Figma.) */
706    font-size: clamp(24px, 2.5vw, 60px);
707    line-height: 1.1;
708    letter-spacing: 0.04em;
709    color: var(--ink);
710    text-wrap: balance;
711  }
712  .pull-quote blockquote p em { font-style: normal; color: var(--accent); }
713  /* Scroll-locked fill. JS splits the text into letters (--i) and sets --p
714     (0→1) on the blockquote as it travels up the viewport; each letter mixes
715     ink → red once the sweep reaches its index. Before JS runs the <em>
716     lead phrase is red and the rest ink, as in the Figma. */
717  .pull-quote blockquote.is-split p { color: var(--ink); }
718  .pull-quote blockquote.is-split p em { color: inherit; }
719  .pull-quote .w { white-space: nowrap; }
720  .pull-quote .ch {
721    --f: clamp(0, calc(var(--p, 0) * var(--n, 1) - var(--i, 0)), 1);
722    color: color-mix(in srgb, var(--accent) calc(var(--f) * 100%), var(--ink));
723  }
724  .quote-mark {
725    position: absolute;
726    font-family: var(--display);
727    font-weight: 800;
728    font-size: min(4.375em, calc(var(--qpad) * 1.6));   /* 140/32 of the quote, capped by the gutter */
729    line-height: 1;
730    letter-spacing: 0.04em;
731    color: var(--accent);
732    user-select: none;
733  }
734  .quote-mark--open  { right: 0.14em; top: -0.26em; }
735  /* Rotated 180°, so the ink sits at the bottom of its box: pull the box up
736     so the ink hangs just under the last line, like the Figma. */
737  .quote-mark--close { left: 0.18em; top: -0.44em; color: var(--ink); transform: rotate(180deg); }
738  .pull-quote cite {
739    display: block;
740    margin-top: 28px;
741    font-family: var(--sans); font-style: normal; font-weight: 400;
742    font-size: 16px; line-height: 1.4; color: var(--ink);
743  }
744  @media (max-width: 700px) {
745    .quote-mark--open  { top: -0.3em; right: 0.1em; }
746    .quote-mark--close { left: 0.12em; }
747  }
748
749  /* ============================================================
750     SINGLE — artwork + copy, then a 16:9 clip. (Section is a partial.)
751     ============================================================ */
752  .single .section-inner { width: var(--wide-container); }
753  .single .section-lede { max-width: 800px; }
754  .video-player {
755    position: relative;
756    aspect-ratio: 16 / 9;
757    max-width: calc(1080px * var(--k));
758    margin: 32px auto 0;
759    background: var(--panel);
760    overflow: hidden;
761  }
762  .video-player > img { width: 100%; height: 100%; object-fit: cover; }
763  .video-player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
764  .video-play {
765    position: absolute; top: 50%; left: 50%;
766    transform: translate(-50%, -50%);
767    width: 64px; height: 64px; padding: 0; border: 0;
768    background: transparent; cursor: pointer;
769    transition: transform 200ms ease;
770  }
771  .video-play img { width: 100%; height: 100%; }
772  .video-play:hover, .video-play:focus-visible { transform: translate(-50%, -50%) scale(1.08); }
773  .video-play:focus-visible { outline: 2px solid var(--white); outline-offset: 6px; border-radius: 50%; }
774
775  /* ============================================================
776     PROMO CARD — single artwork + copy + two CTAs. Figma 31:495.
777     ============================================================ */
778  .promo-grid {
779    display: grid;
780    gap: clamp(28px, 4vw, 50px);
781    align-items: center;
782    max-width: calc(960px * var(--k));
783    margin: 0 auto;
784    text-align: left;
785  }
786  @media (min-width: 780px) { .promo-grid { grid-template-columns: calc(468px * var(--k)) minmax(0, 1fr); } }
787  .promo-art {
788    position: relative; display: block;
789    aspect-ratio: 1 / 1; width: 100%;
790    border: 2px solid var(--text-2);
791    overflow: hidden; background: var(--white);
792  }
793  .promo-art img { width: 100%; height: 100%; object-fit: cover; transition: transform 400ms ease; }
794  .promo-art:hover img, .promo-art:focus-visible img { transform: scale(1.02); }
795  .promo-art:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
796  .promo-art:hover, .promo-art:focus-visible { box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12); }
797  .single-clip { margin-top: clamp(48px, 6vw, 80px); }
798  .single-signup { margin-top: 32px; }
799  .promo-copy .section-heading { text-align: left; }
800  .promo-copy .section-lede { margin: 0 0 32px; text-align: left; }
801  .promo-ctas { display: flex; flex-wrap: wrap; gap: 10px; }
802
803  /* ============================================================
804     STORE — three product tiles on a dark gradient.
805     ============================================================ */
806  .store-grid {
807    display: grid;
808    grid-template-columns: 1fr;
809    gap: 40px;
810    max-width: calc(960px * var(--k));
811    margin: 48px auto 0;
812  }
813  @media (min-width: 640px) { .store-grid { grid-template-columns: repeat(3, 1fr); gap: 30px; } }
814  .product {
815    display: flex; flex-direction: column; gap: 16px;
816    padding: 0 0 24px;
817    text-align: left; color: inherit; text-decoration: none;
818    transition: transform 200ms ease;
819  }
820  a.product:hover, a.product:focus-visible { transform: translateY(-2px); outline: none; }
821  a.product:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
822  .product-img {
823    aspect-ratio: 1 / 1; width: 100%;
824    background: linear-gradient(224.58deg, #423823 3.17%, #241F13 95%);
825    display: flex; align-items: center; justify-content: center;
826    overflow: hidden;
827  }
828  .product-img img { width: 100%; height: 100%; object-fit: cover; }
829  .product-meta { font-weight: 500; font-size: calc(11px * var(--k)); line-height: 1.4; letter-spacing: 0.02em; text-transform: uppercase; color: var(--text-2); }
830  .product-name {
831    font-family: var(--display); font-weight: 800; font-size: calc(24px * var(--k)); line-height: 1.1; letter-spacing: 0.04em;
832    color: var(--ink);
833  }
834  .product-price { font-weight: 500; font-size: calc(16px * var(--k)); color: var(--ink); }
835  .product .btn { width: 100%; margin-top: auto; }
836  .shop-all-link { display: inline-block; margin: 48px 0 0; font-size: calc(16px * var(--k)); }
837
838  /* ============================================================
839     NEWSLETTER — dark, Mick photo left, copy + CTA right.
840     ============================================================ */
841  .newsletter {
842    position: relative;
843    background: var(--panel);
844    color: var(--white);
845    min-height: min(935px, 90vh);
846    display: flex; align-items: center;
847    isolation: isolate;
848    overflow: hidden;
849  }
850  /* Parallax layer — taller than the section, shifted by JS as the section
851     travels through the viewport. */
852  .newsletter-bg {
853    position: absolute; left: 0; right: 0; top: -15%; height: 130%; z-index: -2;
854    background: url("/announcement-assets/newsletter-bg.webp") center / cover no-repeat;
855    will-change: transform;
856  }
857  .newsletter::before {
858    content: ""; position: absolute; inset: 0; z-index: -1;
859    background: linear-gradient(90deg, rgba(16, 16, 15, 0.1) 0%, rgba(16, 16, 15, 0.55) 55%, rgba(16, 16, 15, 0.75) 100%);
860  }
861  .newsletter-inner {
862    width: var(--wide-container);
863    margin: 0 auto;
864    padding: clamp(64px, 10vw, 140px) 0;
865    display: flex; justify-content: flex-end;
866  }
867  .newsletter-panel { width: min(calc(492px * var(--k)), 100%); text-align: left; }
868  .newsletter-panel h2 { font-size: clamp(32px, 4vw, calc(40px * var(--k))); margin-bottom: 12px; }
869  .newsletter-panel .lede { font-size: calc(18px * var(--k)); line-height: 1.5; margin: 0 0 24px; }
870  .newsletter-panel .micro { text-align: center; color: var(--icon-muted); }
871  @media (max-width: 780px) {
872    .newsletter-bg { background-image: url("/announcement-assets/newsletter-bg-960.webp"); background-position: 30% center; }
873    .newsletter::before { background: linear-gradient(180deg, rgba(16, 16, 15, 0.35) 0%, rgba(16, 16, 15, 0.8) 60%); }
874    .newsletter-inner { justify-content: center; }
875    .newsletter-panel { text-align: center; }
876  }
877
878  /* ============================================================
879     FOOTER — Figma 31:635.
880     ============================================================ */
881  .site-footer {
882    background: var(--footer);
883    color: var(--white);
884    padding: clamp(48px, 6vw, 80px) 0 calc(clamp(40px, 6vw, 80px) + env(safe-area-inset-bottom));
885  }
886  .footer-inner { width: var(--wide-container); margin: 0 auto; display: flex; flex-direction: column; gap: 56px; }
887  .footer-primary { display: flex; justify-content: space-between; align-items: flex-start; gap: 32px; flex-wrap: wrap; }
888  .footer-heading {
889    font-family: var(--wide); font-weight: 700; font-size: calc(24px * var(--k)); line-height: calc(32px * var(--k));
890    letter-spacing: 0; text-transform: uppercase; color: var(--white); margin: 0 0 22px;
891  }
892  .footer-socials { display: flex; gap: 18px; align-items: center; }
893  .footer-socials a { display: inline-flex; width: 22px; height: 22px; position: relative; }
894  .footer-socials a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
895  .footer-socials img { width: 22px; height: 22px; transition: opacity 160ms ease; }
896  .footer-socials a:hover img { opacity: 0.7; }
897  .footer-socials a::after {
898    content: attr(aria-label);
899    position: absolute; bottom: calc(100% + 10px); left: 50%;
900    transform: translateX(-50%) translateY(4px);
901    background: var(--white); color: var(--ink);
902    padding: 5px 9px; font-size: calc(11px * var(--k)); font-weight: 500; line-height: 1; white-space: nowrap; border-radius: 2px;
903    opacity: 0; pointer-events: none; transition: opacity 180ms ease, transform 180ms ease;
904  }
905  .footer-socials a:hover::after, .footer-socials a:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }
906  .footer-stay { text-align: right; }
907  .footer-stay a { font-weight: 500; font-size: calc(16px * var(--k)); line-height: calc(20px * var(--k)); letter-spacing: 0; text-transform: uppercase; }
908  .footer-stay a:hover, .footer-stay a:focus-visible { color: var(--accent); }
909  .footer-secondary a { text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 4px; transition: color 180ms ease, text-decoration-color 180ms ease; }
910  .footer-secondary {
911    display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
912    font-size: calc(14px * var(--k)); line-height: calc(20px * var(--k)); color: var(--icon-muted);
913  }
914  .footer-secondary a:hover, .footer-secondary a:focus-visible { color: var(--white); text-decoration-color: currentColor; }
915  .footer-legal { font-size: calc(12px * var(--k)); line-height: calc(18px * var(--k)); letter-spacing: 0; color: var(--icon-muted); text-align: center; }
916  @media (max-width: 640px) {
917    .footer-primary { flex-direction: column; }
918    .footer-stay { text-align: left; }
919  }
920
921  /* ============================================================
922     SIGN-UP SHEET — native <dialog>
922, bottom sheet. Holds a second
923     OpenStage iframe, created on first open. Opened by date rows
924     in the pre-sale state, the newsletter CTA and the footer link.
925     ============================================================ */
926  .signup-sheet {
927    padding: 0; border: 0; background: transparent;
928    max-width: 100vw; width: 100vw; margin: 0;
929    position: fixed; inset: auto 0 0 0; top: auto;
930    color: var(--ink);
931    overflow: visible;
932    transform: translateY(100%);
933    transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1),
934                overlay 340ms ease allow-discrete, display 340ms ease allow-discrete;
935  }
936  .signup-sheet[open] { transform: translateY(0); }
937  @starting-style { .signup-sheet[open] { transform: translateY(100%); } }
938  /* Closing: JS adds .is-closing, waits for the transition, then close().
939     Without that, close() drops the dialog out of the top layer instantly
940     and the backdrop just vanishes. */
941  .signup-sheet[open].is-closing { transform: translateY(100%); }
942  .signup-sheet::backdrop {
943    background: rgba(29, 29, 27, 0);
944    transition: background-color 300ms ease, overlay 340ms ease allow-discrete, display 340ms ease allow-discrete;
945  }
946  .signup-sheet[open]::backdrop { background: rgba(29, 29, 27, 0.45); }
947  .signup-sheet[open].is-closing::backdrop { background: rgba(29, 29, 27, 0); }
948  @starting-style { .signup-sheet[open]::backdrop { background: rgba(29, 29, 27, 0); } }
949  .sheet-panel {
950    background: var(--ground);
951    border-radius: 16px 16px 0 0;
952    padding: 28px clamp(16px, 4vw, 40px) calc(24px + env(safe-area-inset-bottom));
953    display: flex; flex-direction: column; gap: 12px;
954    max-width: calc(580px * var(--k)); margin: 0 auto;
955    max-height: calc(100dvh - 24px); overflow-y: auto;
956    box-shadow: 0 -20px 60px rgba(0, 0, 0, 0.35);
957    text-align: center;
958  }
959  .sheet-panel h2 { font-size: clamp(32px, 5vw, calc(40px * var(--k))); }
960  .sheet-panel .lede { font-size: calc(16px * var(--k)); line-height: 1.5; color: var(--text-2); }
961  .sheet-panel .os-embed { margin-top: 12px; }
962  .sheet-close {
963    appearance: none; display: block; margin: 4px auto 0;
964    background: transparent; border: 0; color: var(--text-3);
965    font-family: var(--sans); font-size: calc(15px * var(--k)); padding: 8px 16px; cursor: pointer;
966    text-decoration: underline; text-underline-offset: 4px;
967  }
968  .sheet-close:hover, .sheet-close:focus-visible { color: var(--ink); outline: none; }
969  .sheet-x {
970    position: absolute; top: 12px; right: 12px;
971    width: 40px; height: 40px; padding: 0; border: 0; border-radius: 50%;
972    background: transparent; color: var(--text-3); font-size: calc(28px * var(--k)); line-height: 1; cursor: pointer;
973    display: flex; align-items: center; justify-content: center;
974  }
975  .sheet-x:hover, .sheet-x:focus-visible { color: var(--ink); background: rgba(29, 29, 27, 0.06); outline: none; }
976  .sheet-panel { position: relative; }
977
978  /* ============================================================
979     VENDOR PICKER — a show with more than one ticket outlet in
980     Bandsintown (V4) opens this instead of linking straight
981     through. Ported from simpleminds.com, re-dressed in SR tokens.
982     ============================================================ */
983  .vendor-modal {
984    padding: 0; border: 0; background: transparent; color: var(--ink);
985    width: min(calc(460px * var(--k)), calc(100vw - 32px)); max-width: calc(100vw - 32px);
986    margin: auto; height: fit-content; max-height: calc(100dvh - 32px);
987    opacity: 0; scale: 0.96;
988    transition: opacity 220ms ease, scale 260ms cubic-bezier(0.22, 1, 0.36, 1),
989                overlay 260ms ease allow-discrete, display 260ms ease allow-discrete;
990  }
991  .vendor-modal[open] { opacity: 1; scale: 1; }
992  @starting-style { .vendor-modal[open] { opacity: 0; scale: 0.96; } }
993  .vendor-modal::backdrop {
994    background: rgba(29, 29, 27, 0);
995    transition: background-color 260ms ease, overlay 260ms ease allow-discrete, display 260ms ease allow-discrete;
996  }
997  .vendor-modal[open]::backdrop { background: rgba(29, 29, 27, 0.45); }
998  @starting-style { .vendor-modal[open]::backdrop { background: rgba(29, 29, 27, 0); } }
999  .vendor-panel {
1000    position: relative; background: var(--ground); border-radius: 12px;
1001    padding: 28px clamp(20px, 4vw, 32px) 20px;
1002    max-height: calc(100dvh - 32px); overflow-y: auto;
1003    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.3);
1004  }
1005  .vendor-panel h2 { font-size: clamp(30px, 4.5vw, calc(36px * var(--k))); margin: 0 48px 6px 0; }
1006  .vendor-show { margin: 0 0 18px; font-size: calc(15px * var(--k)); line-height: 1.4; color: var(--text-2); }
1007  .vendor-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid rgba(29, 29, 27, 0.14); }
1008  .vendor-link {
1009    display: flex; align-items: center; justify-content: space-between; gap: 16px;
1010    padding: 16px 4px; border-bottom: 1px solid rgba(29, 29, 27, 0.14);
1011    color: var(--ink); text-decoration: none;
1012  }
1013  .vendor-link:hover, .vendor-link:focus-visible { background: rgba(29, 29, 27, 0.04); outline: none; border-radius: 0; }
1014  .vendor-link:focus-visible .vendor-name { text-decoration: underline; text-underline-offset: 4px; }
1015  .vendor-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
1016  .vendor-name { font-weight: 500; font-size: calc(17px * var(--k)); line-height: 1.25; }
1017  .vendor-meta { font-size: calc(13px * var(--k)); line-height: 1.3; color: var(--text-3); }
1018  .vendor-arrow { flex: 0 0 auto; width: 18px; height: 18px; color: var(--accent); }
1019  .vendor-modal .sheet-close { margin-top: 12px; }
1020
1021  /* ============================================================
1022     COOKIE banner — same pattern as simpleminds.com. GTM only
1023     loads after "Got it".
1024     ============================================================ */
1025  .cookie-banner {
1026    position: fixed; left: 50%; bottom: clamp(16px, 2vw, 32px);
1027    transform: translateX(-50%) translateY(20px);
1028    width: min(440px, calc(100vw - 32px));
1029    background: var(--panel); color: var(--white);
1030    padding: clamp(24px, 3vw, 36px);
1031    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.2);
1032    opacity: 0; pointer-events: none;
1033    transition: opacity 500ms ease, transform 500ms ease;
1034    z-index: 90;
1035    display: flex; flex-direction: column; gap: 24px;
1036  }
1037  .cookie-banner.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
1038  @media (min-width: 900px) {
1039    .cookie-banner { left: auto; right: clamp(16px, 2vw, 32px); transform: translateY(20px); }
1040    .cookie-banner.is-visible { transform: translateY(0); }
1041  }
1042  .cookie-copy { font-size: calc(16px * var(--k)); line-height: 1.4; }
1043  .cookie-copy a { display: inline-block; margin-top: 12px; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; opacity: 0.85; }
1044  .cookie-copy a:hover, .cookie-copy a:focus-visible { opacity: 1; color: var(--accent); }
1045  .cookie-accept {
1046    appearance: none; border: 0; width: 100%; min-height: 54px; padding: 16px 24px;
1047    background: var(--accent); color: var(--white);
1048    font-family: var(--sans); font-size: calc(16px * var(--k)); font-weight: 500; line-height: 1.4; cursor: pointer; border-radius: 2px;
1049    transition: background-color 160ms ease;
1050  }
1051  .cookie-accept:hover { background: var(--accent-dark); }
1052  .cookie-accept:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
1053
1054  @media (prefers-reduced-motion: reduce) {
1055    html { scroll-behavior: auto; }
1056    .intro .navbar, .intro .band, .intro .cue { opacity: 1; animation: none; }
1057    .intro .wipe { -webkit-mask-position: 0% 0; mask-position: 0% 0; animation: none; }
1058    .armed .r { opacity: 1; transform: none; transition: none; }
1059    .js .reveal { opacity: 1; transform: none; transition: none; }
1060    .cue { display: none; }
1061    .signup-sheet, .signup-sheet::backdrop, .cookie-banner { transition: none; }
1062  }
1063</style>
1064</head>
1065<body>
1066<div id="page" class="intro">
1067
1068<a class="skip-link" href="#signup">Skip to sign-up</a>
1069
1070<!-- Fixed nav pill. "simplyred.com →" goes to the real WP homepage via the
1071     ?full=1 bypass (see the mu-plugin). Plain "/" would loop back here. -->
1072<div class="navbar">
1073  <nav class="nav" aria-label="Page sections">
1074    <a class="is-active" href="#tour" data-nav="tour" aria-current="true">Tour</a>
1075    <a href="#store" data-nav="music">Music</a>
1076    <a class="is-out" href="/?full=1" data-continue>simplyred.com <span aria-hidden="true">&#8594;</span></a>
1077  </nav>
1078</div>
1079
1080<!-- Crawler-visible nav — existing WP URLs -->
1081<nav class="vh" aria-label="Simply Red site sections">
1082  <ul>
1083    <li><a href="/?full=1">Home</a></li>
1084    <li><a href="/tour-dates/">Tour dates</a></li>
1085    <li><a href="/live-in-santiago/">Live in Santiago</a></li>
1086    <li><a href="/recollections/">Recollections</a></li>
1087    <li><a href="/archive/">Archive</a></li>
1088    <li><a href="https://store.simplyred.com/">Shop</a></li>
1089    <li><a href="https://www.simplyred.com/privacy-policy/">Privacy policy</a></li>
1090    <li><a href="https://www.simplyred.com/terms-conditions/">Terms and conditions</a></li>
1091  </ul>
1092</nav>
1093
1094<main>
1095
1096<!-- ==============================================================
1097     HERO — Dan's prototype. Sticky stage; bands scatter in on load
1098     and straighten on scroll. Then the page carries on.
1099     ============================================================== -->
1100<header class="hero-scroll" id="heroScroll" aria-label="Simply Red — Summer Funk ’27">
1101  <div class="hero-sticky">
1102    <div class="stage" id="stage">
1103      <h1 class="vh">Simply Red — Summer Funk ’27 Tour</h1>
1104
1105      <div class="logo"><img class="wipe" src="/announcement-assets/logo.svg" alt="" width="608" height="66" fetchpriority="high"></div>
1106      <div class="script"><img class="wipe" src="/announcement-assets/script.svg" alt="" width="437" height="82"></div>
1107
1108      <div class="kv" id="kv" aria-hidden="true">
1109        <div class="band b-purple"><div class="band-inner"><img src="/announcement-assets/band-purple.webp" alt="" width="170" height="756"></div></div>
1110        <div class="band b-yellow"><div class="band-inner"><img src="/announcement-assets/band-yellow.webp" alt="" width="168" height="756"></div></div>
1111        <div class="band b-red"><div class="band-inner"><img src="/announcement-assets/band-red.webp" alt="" width="166" height="688"></div></div>
1112        <div class="band b-green"><div class="band-inner"><img src="/announcement-assets/band-green.webp" alt="" width="146" height="760"></div></div>
1113        <div class="band b-blue"><div class="band-inner"><img src="/announcement-assets/band-blue.webp" alt="" width="146" height="752"></div></div>
1114        <div class="band b-orange"><div class="band-inner"><img src="/announcement-assets/band-orange.webp" alt="" width="168" height="756"></div></div>
1115        <div class="band b-mick"><div class="band-inner"><img src="/announcement-assets/mick.webp" alt="" width="984" height="1011" fetchpriority="high"></div></div>
1116      </div>
1117
1118      <a class="cue" id="cue" href="#signup">
1119        <span>Scroll</span>
1120        <svg width="15" height="30" viewBox="0 0 15.0604 30.2737" fill="none" aria-hidden="true">
1121          <path d="M14.4764 22.3531C14.2631 22.1399 13.9158 22.1399 13.7025 22.3531L7.52679 28.5289L1.35104 22.3531C1.13639 22.1467 0.794611 22.1494 0.584051 22.36C0.373505 22.5705 0.370771 22.9123 0.577215 23.127L7.13972 29.6895C7.353 29.9028 7.70028 29.9028 7.91353 29.6895L14.476 23.127C14.6893 22.9137 14.6896 22.5664 14.4764 22.3531Z" fill="currentColor" stroke="currentColor" stroke-width="0.848485" stroke-linejoin="bevel"/>
1122          <path d="M14.476 7.04064L7.91353 0.478144C7.70025 0.264861 7.35297 0.264861 7.13972 0.478144L0.577215 7.04064C0.370771 7.25529 0.373505 7.59707 0.584051 7.80763C0.794597 8.01818 1.13639 8.02091 1.35104 7.81447L7.52679 1.63872L13.7025 7.81447C13.9172 8.02091 14.259 8.01818 14.4695 7.80763C14.6801 7.59709 14.6825 7.2553 14.476 7.04064Z" fill="currentColor" stroke="currentColor" stroke-width="0.636364"/>
1123          <path d="M11.3557 15.0837C11.3557 13.536 10.4233 12.1401 8.99319 11.5469C7.56176 10.9549 5.91564 11.2817 4.82049 12.3768C3.72534 13.4719 3.39862 15.118 3.99061 16.5495C4.58396 17.9796 5.97987 18.912 7.52736 18.912C9.64104 18.9093 11.3529 17.1975 11.3557 15.0837ZM4.79319 15.0837C4.79319 13.9776 5.459 12.9809 6.48166 12.5571C7.50296 12.1346 8.67872 12.3684 9.46076 13.1505C10.2428 13.9325 10.4766 15.1083 10.0541 16.1295C9.6303 17.1522 8.6336 17.818 7.52753 17.818C6.01816 17.8167 4.79456 16.5931 4.79319 15.0837Z" fill="currentColor" stroke="currentColor" stroke-width="0.848485" stroke-linejoin="bevel"/>
1124        </svg>
1125      </a>
1126    </div>
1127  </div>
1128</header>
1129
1130<!-- ==============================================================
1131     SIGN-UP — "First to know". The OpenStage form does the capture.
1132     ============================================================== -->
1133<section class="signup" id="signup" aria-labelledby="signupHeading">
1134  <div class="signup-inner">
1135    <h2 class="r" id="signupHeading" style="--d:0ms">First <em>to know</em></h2>
1136    <p class="lede r" style="--d:90ms"
1137       data-copy-announce="Sign up for first access to the artist pre-sale, plus tour news, extra dates and ticket releases."
1138       data-copy-presale="The artist pre-sale is open now. Sign up for access, plus tour news, extra dates and ticket releases."
1139       data-copy-onsale-uk="The EU artist pre-sale is open now. Sign up for access, plus tour news, extra dates and ticket releases."
1140       data-copy-onsale="Sign up for tour news, extra dates and ticket releases.">
1141      Sign up for first access to the artist pre-sale, plus tour news, extra dates and ticket releases.
1142    </p>
1143
1144    <div class="os-embed r" style="--d:180ms">
1145      <!-- OpenStage embed, as supplied. loading="lazy" keeps it out of the
1146           hero's critical path. -->
1147      <iframe
1148        width="500" height="340"
1149        src="https://simplyred.os.fan/website-sign-up-form"
1150        title="Openstage Fan Signup"
1151        loading="lazy"
1152        frameborder="0" scrolling="no"
1153        style="max-width: 100%; color-scheme: none;"
1154        allow="geolocation; microphone; camera; fullscreen; payment"
1155        sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
1156      <noscript><p>Sign up at <a href="https://simplyred.os.fan/website-sign-up-form">simplyred.os.fan</a>.</p></noscript>
1157    </div>
1158
1159    <p class="micro r" style="--d:500ms">No spam &#183; Unsubscribe any time</p>
1160
1161    <p class="share-label r" style="--d:570ms">Send this to whoever you&#8217;re going with</p>
1162    <div class="share" data-share>
1163      <a class="r" style="--d:640ms" data-share-email href="#" rel="noopener">Email</a>
1164      <a class="r" style="--d:690ms" data-share-whatsapp href="#" rel="noopener" target="_blank">WhatsApp</a>
1165      <a class="r" style="--d:740ms" data-share-x href="#" rel="noopener" target="_blank">X</a>
1166      <a class="r" style="--d:790ms" data-share-facebook href="#" rel="noopener" target="_blank">Facebook</a>
1167    </div>
1168  </div>
1169</section>
1170
1171<!-- ==============================================================
1172     TOUR — state-driven lede; trailer; dates by region.
1173     Rows hydrate from Bandsintown. Until the tour is published
1174     there, the list shows a "dates to be announced" line rather
1175     than fake rows. ?mock=1 renders a review dataset instead.
1176     ============================================================== -->
1177<section class="tour reveal" id="tour" aria-labelledby="tourHeading">
1178  <div class="section-inner">
1179    <h2 class="section-heading" id="tourHeading">The Summer Funk &#8216;27 <em>Tour</em></h2>
1180    <p class="section-lede"
1181       data-copy-announce="UK and Europe, Summer 2027. Sign up for first access to the artist pre-sale from 22 September. General sale opens 24 September for the UK shows, and 25 September for the European shows."
1182       data-copy-presale="UK and Europe, Summer 2027. The artist pre-sale is open now — sign up for access. General sale opens 24 September for the UK shows, and 25 September for the European shows."
1183       data-copy-onsale-uk="UK and Europe, Summer 2027. UK tickets are available now. The artist pre-sale for EU is open now — sign up for access. EU General sale opens 25 September for the European shows."
1184       data-copy-onsale="UK and Europe, Summer 2027. Tickets on sale now.">
1185      UK and Europe, Summer 2027. Sign up for first access to the artist pre-sale from 22 September. General sale opens 24 September for the UK shows, and 25 September for the European shows.
1186    </p>
1187
1188    <!-- Tour trailer — 16:9, full-bleed, self-hosted. Click to play, with
1189         sound, from the button centred on the poster (no autoplay — client,
1190         15 Sep). Pauses if scrolled away. The mp4s are placeholders. -->
1191    <figure class="tour-video" data-tour-video>
1192      <img class="tour-video-poster" src="/announcement-assets/tour-trailer-poster-v2.webp" alt="" width="1600" height="1067" loading="lazy">
1193      <video
1194        data-trailer-video
1195        data-src-large="https://sr-summer-funk-media.eleven-eleven-studio.workers.dev/trailer-1080.mp4"
1196        data-src-small="https://sr-summer-funk-media.eleven-eleven-studio.workers.dev/trailer-720.mp4"
1197        muted loop playsinline preload="none"
1198        poster="/announcement-assets/tour-trailer-poster-v2.webp"
1199        aria-label="Simply Red — Summer Funk ’27 tour trailer"
1200        hidden>
1201        <source data-trailer-source type="video/mp4">
1202      </video>
1203      <div class="tour-video-overlay">
1204        <img src="/announcement-assets/logo.svg" alt="" width="501" height="55" aria-hidden="true">
1205        <img src="/announcement-assets/script-white.svg" alt="" width="360" height="68" aria-hidden="true">
1206        <button type="button" class="btn" data-trailer-play>Watch the tour trailer</button>
1207      </div>
1208      <button type="button" class="video-mute" data-trailer-mute aria-pressed="false" aria-label="Unmute trailer" hidden>
1209        <svg class="icon-muted" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zm13.59 3L14 9.41 15.41 8 18 10.59 20.59 8 22 9.41 19.41 12 22 14.59 20.59 16 18 13.41 15.41 16 14 14.59 16.59 12z"/></svg>
1210        <svg class="icon-unmuted" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
1211      </button>
1212      <figcaption class="vh">Simply Red — Summer Funk ’27 tour trailer</figcaption>
1213    </figure>
1214
1215    <div class="dates-list" data-dates-list id="tour-dates" aria-live="polite">
1216      <!-- Rendered by JS from Bandsintown. ?mock=1 renders a review dataset
1217           through the same pipeline; nothing here is real until BIT is. -->
1218      <p class="dates-empty">Dates to be announced shortly.</p>
1219    </div>
1220    <button type="button" class="btn dates-toggle" data-dates-toggle aria-expanded="false" aria-controls="tour-dates" hidden>
1221      <span data-dates-toggle-label>See all dates &#8250;</span>
1222    </button>
1223  </div>
1224</section>
1225
1226<!-- ==============================================================
1227     QUOTE — Mick on the tour.
1228     ============================================================== -->
1229<section class="pull-quote" aria-label="A word from Mick Hucknall">
1230  <div class="section-inner">
1231    <blockquote>
1232      <p><span class="quote-mark quote-mark--open" aria-hidden="true">&#8220;</span><em>We can&#8217;t wait to get back out on the road again in Summer 2027</em> to perform at some incredible and historic venues across the UK and Europe. You can expect a set full of Simply Red upbeat hits to get you on your feet having fun in the sun with friends and family.<span class="quote-mark quote-mark--close" aria-hidden="true">&#8220;</span></p>
1233      <cite>Mick Hucknall</cite>
1234    </blockquote>
1235  </div>
1236</section>
1237
1238<!-- ==============================================================
1239     REACH OUT I’LL BE THERE — the single. Order per client (15 Sep):
1240     artwork + Coming soon + Pre-save → the clip → sign-up line → Mick.
1241     Release day: at SR.SINGLE_OUT_AT the copy flips to Out now + Listen now
1242     (same smart link). ?single=soon|out forces either for review.
1243     The clip plays via YouTube once SR.REACH_OUT_YOUTUBE_ID is set; until
1244     then the play badge is hidden.
1245     ============================================================== -->
1246<section class="single reveal" id="single" aria-labelledby="promoHeading">
1247  <div class="section-inner">
1248    <div class="promo-grid">
1249      <a class="promo-art" href="https://simplyred.lnk.to/ReachOut" rel="noopener" target="_blank" aria-label="Pre-save Reach Out I’ll Be There" data-single-art>
1250        <img src="/announcement-assets/reachout-artwork.webp" alt="Reach Out I’ll Be There — Simply Red single artwork" width="1200" height="1200" loading="lazy">
1251      </a>
1252      <div class="promo-copy">
1253        <h2 class="section-heading" id="promoHeading">Coming <em>soon</em></h2>
1254        <p class="section-lede" data-single-lede>Simply Red&#8217;s live take on the Four Tops classic, released on 24 September.</p>
1255        <div class="promo-ctas">
1256          <a class="btn" href="https://simplyred.lnk.to/ReachOut" data-presave rel="noopener" target="_blank">Pre-save</a>
1257        </div>
1258      </div>
1259    </div>
1260
1261    <!-- Clip + its sign-up line: hidden until SR.CLIP_AT (?clip=1 previews). -->
1262    <div class="single-clip-wrap" data-single-clip hidden>
1263      <div class="video-player single-clip" data-video-player>
1264        <img src="/announcement-assets/reachout-still-v3.webp" alt="Mick Hucknall performing Reach Out I’ll Be There live on stage" width="1280" height="720" loading="lazy">
1265        <button type="button" class="video-play" data-video-play aria-label="Play the Reach Out I’ll Be There clip">
1266          <img src="/announcement-assets/play-btn.svg" alt="" width="64" height="64">
1267        </button>
1268      </div>
1269
1270      <p class="section-lede single-signup">Sign up to be the first to watch the full live performance of &#8216;Reach Out I&#8217;ll Be There&#8217;, filmed on the 40th anniversary tour, 2025.</p>
1271      <p class="section-cta"><button type="button" class="btn" data-open-sheet data-sheet-context="single">Sign up</button></p>
1272    </div>
1273  </div>
1274</section>
1275
1276<!-- ==============================================================
1277     QUOTE — Mick on the single.
1278     ============================================================== -->
1279<section class="pull-quote" aria-label="Mick Hucknall on Reach Out I’ll Be There">
1280  <div class="section-inner">
1281    <blockquote>
1282      <p><span class="quote-mark quote-mark--open" aria-hidden="true">&#8220;</span><em>We&#8217;re really pleased to bring you our live cover of &#8216;Reach Out I&#8217;ll Be There&#8217;.</em> We had a great time performing it on our 40th anniversary tour last year, and it will help keep you going until you can see Simply Red live again next Summer.<span class="quote-mark quote-mark--close" aria-hidden="true">&#8220;</span></p>
1283      <cite>Mick Hucknall</cite>
1284    </blockquote>
1285  </div>
1286</section>
1287
1288<script>
1289  /* Reach Out — config + clip player. Lives in the partial so nothing about
1290     the single is in the page until the partial is inlined. The partial sits
1291     above the page's CONFIG script, so wait for it: SR isn't defined yet. */
1292  document.addEventListener("DOMContentLoaded", function () {
1293    SR.REACH_OUT_YOUTUBE_ID = "xVHUZCqs_WI";                 // "Reach Out (Live Version) Trailer", unlisted
1294    SR.CLIP_AT = "2026-09-24T09:45:00+01:00";                // clip + sign-up line appear, BST (brought forward 24 Sep)
1295    var clipWrap = document.querySelector("[data-single-clip]");
1296    if (clipWrap && (/[?&]clip=1/.test(location.search) || Date.now() >= Date.parse(SR.CLIP_AT))) clipWrap.hidden = false;
1297    SR.PRESAVE_URL = "https://simplyred.lnk.to/ReachOut";
1298    SR.SINGLE_OUT_AT = "2026-09-24T00:00:00+01:00";         // release, BST
1299    var forced = /[?&]single=(soon|out)/i.exec(location.search);
1300    var out = forced ? forced[1].toLowerCase() === "out" : Date.now() >= Date.parse(SR.SINGLE_OUT_AT);
1301    if (out) {
1302      var h = document.getElementById("promoHeading");
1303      var lede = document.querySelector("[data-single-lede]");
1304      var art = document.querySelector("[data-single-art]");
1305      var btn = document.querySelector("[data-presave]");
1306      if (h) h.innerHTML = "Out <em>now</em>";
1307      if (lede) lede.innerHTML = "Simply Red&#8217;s live take on the Four Tops classic, out now.";
1308      if (art) art.setAttribute("aria-label", "Listen to Reach Out I’ll Be There");
1309      if (btn) btn.textContent = "Listen now";
1310    }
1311    SR.SHEET_COPY = SR.SHEET_COPY || {};
1312    SR.SHEET_COPY.single = {
1313      h: "Be the first <em>to watch</em>",
1314      p: "Sign up for free and we’ll send you the full live performance of ‘Reach Out I’ll Be There’ as soon as it’s released, plus tour news and ticket releases."
1315    };
1316    var player = document.querySelector("[data-video-player]");
1317    var play = document.querySelector("[data-video-play]");
1318    var presave = document.querySelector("[data-presave]");
1319    if (presave && SR.PRESAVE_URL) presave.setAttribute("href", SR.PRESAVE_URL);
1320
1321    // No clip URL yet → the play button is hidden. Never route this to the
1322    // sign-up sheet.
1323    if (!SR.REACH_OUT_YOUTUBE_ID && play) play.hidden = true;
1324
1325    function playClip() {
1326      if (!SR.REACH_OUT_YOUTUBE_ID) return;
1327      if (!player || player.querySelector("iframe")) return;
1328      var f = document.createElement("iframe");
1329      f.setAttribute("src", "https://www.youtube-nocookie.com/embed/" + SR.REACH_OUT_YOUTUBE_ID + "?autoplay=1&rel=0&modestbranding=1");
1330      f.setAttribute("title", "Simply Red — Reach Out I’ll Be There");
1331      f.setAttribute("allow", "autoplay; encrypted-media; picture-in-picture; fullscreen");
1332      f.setAttribute("allowfullscreen", "");
1333      player.appendChild(f);
1334      if (play) play.hidden = true;
1335      window.SR_track("sr_clip_played", { clip: "single" });
1336    }
1337    if (play) play.addEventListener("click", playClip);
1338  });
1339
1340</script>
1340
1341
1342<!-- ==============================================================
1343     STORE — three tiles. Titles, prices, URLs and "limited edition"
1344     flags checked against the live store pages 15 Sep 2026 (packshots
1345     are the store's own). "Buy now", not "Add to cart": no cart here.
1346     ============================================================== -->
1347<section class="store reveal" id="store" aria-labelledby="storeHeading">
1348  <div class="section-inner">
1349    <h2 class="section-heading" id="storeHeading">The official <em>store</em></h2>
1350    <p class="section-lede">Signed editions, coloured vinyl and bundles you will not find anywhere else.</p>
1351
1352    <div class="store-grid">
1353      <a class="product" href="https://store.simplyred.com/product/holding-back-the-years-artbook" rel="noopener" target="_blank">
1354        <div class="product-img"><img src="/announcement-assets/product-santiago-artbook.webp" alt="Holding Back The Years: 40 Years of Simply Red, Live in Santiago — Collectors Edition 4-disc artbook" width="800" height="800" loading="lazy"></div>
1355        <p class="product-meta">Limited edition</p>
1356        <p class="product-name">Holding Back The Years: 40 Years of Simply Red, Live in Santiago Collectors Edition 4Disc Artbook</p>
1357        <p class="product-price">&pound;70</p>
1358        <span class="btn">Buy now</span>
1359      </a>
1360      <a class="product" href="https://store.simplyred.com/product/holding-back-the-years-marble-vinyl-lp-triple-heavyweight-lp" rel="noopener" target="_blank">
1361        <div class="product-img"><img src="/announcement-assets/product-santiago-vinyl.webp" alt="Holding Back The Years: 40 Years of Simply Red, Live in Santiago — Special Edition marble vinyl 3LP" width="800" height="800" loading="lazy"></div>
1362        <p class="product-meta">Limited edition</p>
1363        <p class="product-name">Holding Back The Years: 40 Years of Simply Red, Live in Santiago Special Edition Marble Vinyl 3LP</p>
1364        <p class="product-price">&pound;55</p>
1365        <span class="btn">Buy now</span>
1366      </a>
1367      <a class="product" href="https://simplyred.tmstor.es/product/recollections" rel="noopener" target="_blank">
1368        <div class="product-img"><img src="/announcement-assets/product-recollections-3cd.webp" alt="Recollections 3CD" width="800" height="800" loading="lazy"></div>
1369        <p class="product-meta">3CD</p>
1370        <p class="product-name">Recollections 3CD</p>
1371        <p class="product-price">&pound;15</p>
1372        <span class="btn">Buy now</span>
1373      </a>
1374    </div>
1375
1376    <a class="text-link shop-all-link" href="https://store.simplyred.com/" rel="noopener" target="_blank">Shop the full Simply Red store &#8250;</a>
1377  </div>
1378</section>
1379
1380</main>
1381
1382<!-- ==============================================================
1383     NEWSLETTER — dark. One CTA into the sign-up sheet rather than a
1384     second drawn form (OpenStage owns the fields).
1385     ============================================================== -->
1386<section class="newsletter reveal" id="newsletter" aria-labelledby="newsletterHeading">
1387  <div class="newsletter-bg" data-parallax aria-hidden="true"></div>
1388  <div class="newsletter-inner">
1389    <div class="newsletter-panel">
1390      <h2 class="section-heading" id="newsletterHeading">Never miss a <em>Simply Red moment</em></h2>
1391      <p class="lede">Tour news, new music and store drops, straight to your inbox.</p>
1392      <button type="button" class="btn btn--block" data-open-sheet data-sheet-context="newsletter">Sign up now</button>
1393      <p class="micro">No spam &#183; Unsubscribe any time</p>
1394    </div>
1395  </div>
1396</section>
1397
1398<!-- ==============================================================
1399     FOOTER
1400     ============================================================== -->
1401<footer class="site-footer">
1402  <div class="footer-inner">
1403    <div class="footer-primary">
1404      <div>
1405        <p class="footer-heading">Follow Simply Red</p>
1406        <nav class="footer-socials" aria-label="Simply Red on social platforms">
1407          <a href="https://www.facebook.com/simplyred/" rel="noopener" target="_blank" aria-label="Facebook"><img src="/announcement-assets/social/facebook.svg" alt="" width="22" height="22"></a>
1408          <a href="https://www.tiktok.com/@simplyredofficial" rel="noopener" target="_blank" aria-label="TikTok"><img src="/announcement-assets/social/tiktok.svg" alt="" width="22" height="22"></a>
1409          <a href="https://www.instagram.com/simplyredofficial/" rel="noopener" target="_blank" aria-label="Instagram"><img src="/announcement-assets/social/instagram.svg" alt="" width="22" height="22"></a>
1410          <a href="https://www.youtube.com/channel/UCwtvFWbWkLr8ra51W-pLstQ" rel="noopener" target="_blank" aria-label="YouTube"><img src="/announcement-assets/social/youtube.svg" alt="" width="22" height="22"></a>
1411        </nav>
1412      </div>
1413      <div class="footer-stay">
1414        <p class="footer-heading">Stay in touch</p>
1415        <a href="#signup" data-open-sheet data-sheet-context="footer">Sign up for the mailing list</a>
1416      </div>
1417    </div>
1418    <div class="footer-secondary">
1419      <p>&copy; 2026 Simply Red. All rights reserved. &nbsp;<a href="https://www.simplyred.com/terms-conditions/">Terms and Conditions</a> &nbsp;|&nbsp; <a href="https://www.simplyred.com/privacy-policy/">Privacy Policy</a></p>
1420      <p><a href="https://11-11.studio" rel="noopener" target="_blank">Website by 11:11 Studio</a></p>
1421    </div>
1422    <div class="footer-legal">
1423      <p>This site and all the material contained therein &copy; copyright simplyred.com ltd.</p>
1424      <p>simplyred.com ltd (reg no 4260793) is incorporated in England &amp; Wales and is registered</p>
1425      <p>c/o OJK Limited 3rd Floor Colwyn Chambers, York Street, Manchester, M2 3BA. Vat Reg No 785 4256 94</p>
1426    </div>
1427  </div>
1428</footer>
1429
1430</div><!-- /#page -->
1431
1432<!-- ==============================================================
1433     SIGN-UP SHEET — the OpenStage form in a bottom sheet. iframe is
1434     created on first open so the page only ever loads one copy of
1435     the form up front.
1436     ============================================================== -->
1437<dialog class="signup-sheet" data-signup-sheet aria-labelledby="sheetHeading">
1438  <div class="sheet-panel">
1439    <button type="button" class="sheet-x" data-close-sheet aria-label="Close">&times;</button>
1440    <h2 id="sheetHeading" class="display"
1441        data-copy-announce="Pre-sale <em>registration</em>"
1442        data-copy-presale="Pre-sale <em>access</em>"
1443        data-copy-onsale-uk="Pre-sale <em>access</em>"
1444        data-copy-onsale="Stay <em>in touch</em>">Pre-sale <em>registration</em></h2>
1445    <p class="lede"
1446       data-copy-announce="Sign up for free and we’ll send your artist pre-sale access before tickets go on general sale."
1447       data-copy-presale="Sign up for free and we’ll send your artist pre-sale access. General sale opens 24 September for the UK shows, and 25 September for the European shows."
1448       data-copy-onsale-uk="Sign up for free and we’ll send your artist pre-sale access for the European shows. General sale opens 25 September."
1449       data-copy-onsale="Tour news, new music and store drops, straight to your inbox.">
1450      Sign up for free and we’ll send your artist pre-sale access before tickets go on general sale.
1451    </p>
1452    <div class="os-embed" data-sheet-embed>
1453      <iframe
1454        width="500" height="340"
1455        data-src="https://simplyred.os.fan/website-sign-up-form"
1456        title="Openstage Fan Signup"
1457        frameborder="0" scrolling="no"
1458        style="max-width: 100%; color-scheme: none;"
1459        allow="geolocation; microphone; camera; fullscreen; payment"
1460        sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>
1461    </div>
1462    <button type="button" class="sheet-close" data-close-sheet>Close</button>
1463  </div>
1464</dialog>
1465
1466<!-- Vendor picker — filled by JS from the Bandsintown V4 offers for the
1467     clicked show. Only opens when a show lists more than one outlet. -->
1468<dialog class="vendor-modal" data-vendor-modal aria-labelledby="vendorHeading">
1469  <div class="vendor-panel">
1470    <button type="button" class="sheet-x" data-close-vendor aria-label="Close">&times;</button>
1471    <h2 id="vendorHeading" class="display">Buy <em>tickets</em></h2>
1472    <p class="vendor-show" data-vendor-show></p>
1473    <ul class="vendor-list" data-vendor-list></ul>
1474    <button type="button" class="sheet-close" data-close-vendor>Cancel</button>
1475  </div>
1476</dialog>
1477
1478<!-- Cookie consent -->
1479<div class="cookie-banner" role="region" aria-label="Cookie consent" aria-live="polite" hidden>
1480  <p class="cookie-copy">
1481    This website uses cookies to ensure you get the best experience on our website.
1482    <a href="https://www.simplyred.com/privacy-policy/" rel="noopener">Learn more</a>
1483  </p>
1484  <button type="button" class="cookie-accept" data-cookie-accept>Got it</button>
1485</div>
1486
1487<script>
1488  /* =========================================================
1489     CONFIG — the things that change between now and general
1490     sale live here. Everything else derives from them.
1491     ========================================================= */
1492  var SR = {
1493    // Campaign timeline (BST). Times are placeholders until the promoter
1494    // confirms them — see docs/landing-page-plan.md → Open items.
1495    PRESALE_AT:   "2026-09-22T10:00:00+01:00",
1496    ONSALE_AT_UK: "2026-09-24T10:00:00+01:00",   // UK shows
1497    ONSALE_AT:    "2026-09-25T09:00:00+01:00",   // European shows: 10:00 CEST = 09:00 BST (per-show times in EU_SALES)
1498    // trailer-1080.mp4 / trailer-720.mp4 are PLACEHOLDERS (a 12 s push-in
1499    // on the poster, silent audio track) so the player + sound control are
1500    // reviewable. Replace both files with the real trailer, same names.
1501    TRAILER_READY: true,
1502    // Bandsintown
1503    BIT_APP_ID: "js_www.simplyred.com",
1504    BIT_ARTIST_ID: "6644",
1505    BIT_ARTIST: "Simply Red",
1506    SHARE_URL: "https://www.simplyred.com/",
1507    SHARE_TEXT: "Simply Red — Summer Funk ’27 tour. Sign up for pre-sale access."
1508  };
1509
1510  /* =========================================================
1511     PAGE STATE — announce → presale → onsale-uk → onsale, by clock.
1512     ?state=announce|presale|onsale-uk|onsale forces one for review.
1513     ========================================================= */
1514  function pageState() {
1515    var m = /[?&]state=(announce|presale|onsale-uk|onsale)/i.exec(location.search);
1516    if (m) return m[1].toLowerCase();
1517    var now = Date.now();
1518    if (now >= Date.parse(SR.ONSALE_AT)) return "onsale";
1519    if (now >= Date.parse(SR.ONSALE_AT_UK)) return "onsale-uk";
1520    if (now >= Date.parse(SR.PRESALE_AT)) return "presale";
1521    return "announce";
1522  }
1523  var STATE = pageState();
1524  document.documentElement.setAttribute("data-state", STATE);
1525
1526  // Swap state copy on every element that carries data-copy-<state>.
1527  document.querySelectorAll("[data-copy-" + STATE + "]").forEach(function (el) {
1528    var v = el.getAttribute("data-copy-" + STATE);
1529    if (el.tagName === "H2") el.innerHTML = v; else el.textContent = v;
1530  });
1531
1532  /* =========================================================
1533     HERO — Dan's prototype motion. Band start offsets in KV
1534     space from Figma 47:2253. They resolve over the first ~45% of a screen of scroll.
1535     ========================================================= */
1536  (function () {
1537    var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1538    var page = document.getElementById("page");
1539    var stage = document.getElementById("stage");
1540    var scroller = document.getElementById("heroScroll");
1541    var sticky = scroller.querySelector(".hero-sticky");
1542    var kv = document.getElementById("kv");
1543    var cue = document.getElementById("cue");
1544    var signup = document.getElementById("signup");
1545
1546    var BANDS = [
1547      { sel: ".b-orange", dx: -159.99, rot: -6.31, end: 1.00 },
1548      { sel: ".b-blue",   dx: -102.02, rot:  3.09, end: 0.82 },
1549      { sel: ".b-green",  dx:  -35.05, rot: -2.68, end: 0.68 },
1550      { sel: ".b-red",    dx:   53.00, rot: -2.92, end: 0.72 },
1551      { sel: ".b-yellow", dx:  136.03, rot: -3.06, end: 0.86 },
1552      { sel: ".b-purple", dx:  179.96, rot:  8.75, end: 1.00 },
1553      { sel: ".b-mick",   dx: 0, dy: 70, rot: 0,   end: 0.90 }
1554    ];
1555    BANDS.forEach(function (b) { b.el = stage.querySelector(b.sel + " .band-inner"); });
1556
1557    function easeOutQuart(t) { return 1 - Math.pow(1 - t, 4); }
1558    function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
1559
1560    var MOBILE = false, KV_S = 1;
1561    // Phone rest positions: the outer strips slide inward so all six stay
1562    // on screen at a scale where Mick fills the width. KV-space px.
1563    var MOBILE_REST = { ".b-orange": 72, ".b-blue": 30, ".b-yellow": -30, ".b-purple": -72 };
1564    function paint(p) {
1565      for (var i = 0; i < BANDS.length; i++) {
1566        var b = BANDS[i];
1567        var e = easeOutQuart(clamp01(p / b.end));
1568        // Phone: strips rest tucked inward and only scatter ~35% as far,
1569        // so the outer ones stay on screen before the user scrolls.
1570        var rest = MOBILE ? (MOBILE_REST[b.sel] || 0) : 0;
1571        var amp = MOBILE ? 0.35 : 1;
1572        b.el.style.transform =
1573          "translate3d(" + (rest + b.dx * amp * (1 - e)).toFixed(2) + "px," +
1574                           ((b.dy || 0) * (1 - e)).toFixed(2) + "px,0) rotate(" +
1575                           (b.rot * (1 - e)).toFixed(3) + "deg)";
1576      }
1577      var breathe = 1 + 0.016 * (1 - easeOutQuart(p));
1578      kv.style.transform = MOBILE
1579        ? "translateX(-50%) scale(" + (KV_S * breathe).toFixed(4) + ")"
1580        : "scale(" + breathe.toFixed(4) + ")";
1581      if (cue && cue.classList.contains("is-live")) cue.style.opacity = String(clamp01(1 - p / 0.25));
1582    }
1583
1584    /* Fit the 1440×852 stage to the viewport. Desktop: scale so the whole
1585       composition fills the window — up on a big display, down on a small
1586       one (both axes considered, so nothing is cropped). Narrow screens:
1587       scale so the key visual (plus the flying-in bands) fills the width —
1588       the stage is centred, so the logo, script and Mick stay in frame and
1589       the outer band edges crop. Artwork is exported at 3× so it holds up
1590       to ~2.5× scale. */
1591    var fittedW = 0;
1592    function fit(force) {
1593      var w = window.innerWidth, h = window.innerHeight;
1594      // Phones fire resize as the browser chrome collapses on scroll; the
1595      // height changes but the width doesn't. Rebuilding the hero to the
1596      // new height mid-scroll is the "jump" — so on phones only refit when
1597      // the width changes (rotation), and let the layout stand otherwise.
1598      if (!force && w < 720 && w === fittedW) return;
1599      fittedW = w;
1600      var root = document.documentElement.style;
1601      MOBILE = w < 720;
1602      if (MOBILE) {
1603        // Own composition: fill the phone screen. Logo under the nav, key
1604        // visual scaled to the space left and pinned to the bottom. Mick
1605        // (328 wide in KV space) is kept on screen; the outer strips may
1606        // crop on narrow phones, which is fine — they're the frame.
1607        var navEl = document.querySelector(".nav");
1608        var navB = navEl ? navEl.getBoundingClientRect().bottom : 60;
1609        var logoW = Math.min(w * 0.86, 520), logoH = logoW * 66.44 / 608.618;
1610        var scriptW = logoW * 0.72, scriptH = scriptW * 82.21 / 436.825;
1611        var logoTop = navB + 64, scriptTop = logoTop + logoH + 14;
1612        var availH = h - (scriptTop + scriptH + 12);
1613        KV_S = Math.max(0.6, Math.min(availH / 452, (w / 328) * 0.96)) * 0.9;
1614        root.setProperty("--s", 1);
1615        root.setProperty("--navS", 1);
1616        root.setProperty("--stageBase", h + "px");
1617        root.setProperty("--logoTop", logoTop + "px");
1618        root.setProperty("--logoW", logoW + "px");
1619        root.setProperty("--scriptTop", scriptTop + "px");
1620        root.setProperty("--scriptW", scriptW + "px");
1621        paint(current);
1622        return;
1623      }
1624      KV_S = 1;
1625      // The composition (logo + bands at their flung-out start positions)
1626      // spans ~860px of the 1440 stage. Fit that, with a little air, rather
1627      // than the full stage width — otherwise a tablet or small desktop
1628      // window shows the artwork small in the middle of empty ground.
1629      var s = Math.min((w - 32) / 1000, h / 852);
1630      root.setProperty("--s", s);
1631      root.setProperty("--navS", Math.min(1, s));
1632      root.setProperty("--stageBase", (852 * s) + "px");
1633    }
1634
1635    function armReveal() {
1636      if (reduce || !("IntersectionObserver" in window)) return;
1637      signup.classList.add("armed");
1638      new IntersectionObserver(function (entries) {
1639        entries.forEach(function (e) {
1640          if (e.isIntersecting) e.target.classList.add("in");
1641          else if (e.boundingClientRect.top > 0) e.target.classList.remove("in");
1642        });
1643      }, { threshold: 0, rootMargin: "0px 0px -15% 0px" }).observe(signup);
1644    }
1645
1646    var target = 0, current = 0, raf = null;
1647
1648    if (reduce) {
1649      current = 1; fit(true); paint(1);
1650      window.addEventListener("resize", function () { fit(); });
1651      return;
1652    }
1653    function readScroll() {
1654      // Resolve over the first 45% of the hero's height of scroll.
1655      var len = sticky.offsetHeight * 0.45;
1656      target = clamp01(len > 0 ? (window.scrollY || window.pageYOffset || 0) / len : 1);
1657      if (!raf) raf = requestAnimationFrame(tick);
1658    }
1659    function tick() {
1660      current += (target - current) * 0.12;
1661      if (Math.abs(target - current) < 0.0004) current = target;
1662      paint(current);
1663      raf = (current === target) ? null : requestAnimationFrame(tick);
1664    }
1665
1666    if (cue) cue.addEventListener("animationend", function () { cue.classList.add("is-live"); readScroll(); });
1667    fit(true);
1668    paint(0);
1669    armReveal();
1670    readScroll();
1671    window.addEventListener("scroll", readScroll, { passive: true });
1672    window.addEventListener("resize", function () { fit(); readScroll(); });
1673  })();
1674
1675  /* =========================================================
1676     Section reveals + nav active state.
1677     ========================================================= */
1678  (function () {
1679    var els = document.querySelectorAll(".reveal");
1680    if (!("IntersectionObserver" in window)) {
1681      els.forEach(function (el) { el.classList.add("is-visible"); });
1682      return;
1683    }
1684    var io = new IntersectionObserver(function (entries) {
1685      entries.forEach(function (e) {
1686        if (e.isIntersecting) { e.target.classList.add("is-visible"); io.unobserve(e.target); }
1687      });
1688    }, { threshold: 0.08, rootMargin: "0px 0px -60px 0px" });
1689    els.forEach(function (el) { io.observe(el); });
1690
1691    // Nav pill: "Tour" is active from the top down to the Music section
1692    // (the single's section when it's in the page, otherwise the store); "Music" from there on.
1693    var navLinks = document.querySelectorAll("[data-nav]");
1694    var music = document.getElementById("single") || document.getElementById("store");
1695    var musicLink = document.querySelector('[data-nav="music"]');
1696    if (musicLink && music) musicLink.setAttribute("href", "#" + music.id);
1697    function setActive(key) {
1698      navLinks.forEach(function (a) {
1699        var on = a.getAttribute("data-nav") === key;
1700        a.classList.toggle("is-active", on);
1701        if (on) a.setAttribute("aria-current", "true"); else a.removeAttribute("aria-current");
1702      });
1703    }
1704    if (music) {
1705      new IntersectionObserver(function (entries) {
1706        entries.forEach(function (e) {
1707          setActive(e.isIntersecting || e.boundingClientRect.top < 0 ? "music" : "tour");
1708        });
1709      }, { rootMargin: "-40% 0px -55% 0px" }).observe(music);
1710      // Anything past the store is "Music" territory too.
1711      window.addEventListener("scroll", function () {
1712        if (music.getBoundingClientRect().top < window.innerHeight * 0.4) setActive("music"); else setActive("tour");
1713      }, { passive: true });
1714    }
1715  })();
1716
1717  /* =========================================================
1718     Share row — populated from SR.SHARE_URL / SHARE_TEXT.
1719     ========================================================= */
1720  (function () {
1721    var u = encodeURIComponent(SR.SHARE_URL), t = encodeURIComponent(SR.SHARE_TEXT);
1722    var map = {
1723      "[data-share-email]": "mailto:?subject=" + encodeURIComponent("Simply Red — Summer Funk ’27") + "&body=" + t + "%0A%0A" + u,
1724      "[data-share-whatsapp]": "https://wa.me/?text=" + t + "%20" + u,
1725      "[data-share-x]": "https://twitter.com/intent/tweet?text=" + t + "&url=" + u,
1726      "[data-share-facebook]": "https://www.facebook.com/sharer/sharer.php?u=" + u
1727    };
1728    Object.keys(map).forEach(function (sel) {
1729      var a = document.querySelector(sel);
1730      if (a) a.setAttribute("href", map[sel]);
1731    });
1732    var wrap = document.querySelector("[data-share]");
1733    if (wrap) wrap.addEventListener("click", function (e) {
1734      var a = e.target.closest("a"); if (!a) return;
1735      window.SR_track("sr_share_clicked", { share_channel: a.textContent.trim().toLowerCase() });
1736    });
1737  })();
1738
1739  /* =========================================================
1740     Sign-up sheet — native <dialog>. iframe src set on first open.
1741     ========================================================= */
1742  var sheet = document.querySelector("[data-signup-sheet]");
1743  // Sheet heading + lede follow the page state, unless the trigger's context
1744  // has its own copy in SR.SHEET_COPY (set by a partial, e.g. the single).
1745  var sheetHead = sheet && sheet.querySelector("#sheetHeading");
1746  var sheetLede = sheet && sheet.querySelector(".lede");
1747  var sheetDefault = sheet && { h: sheetHead.innerHTML, p: sheetLede.textContent };
1748  function openSheet(context) {
1749    if (!sheet) return;
1750    var copy = (SR.SHEET_COPY && SR.SHEET_COPY[context]) || sheetDefault;
1751    sheetHead.innerHTML = copy.h; sheetLede.textContent = copy.p;
1752    var frame = sheet.querySelector("iframe[data-src]");
1753    if (frame) { frame.setAttribute("src", frame.getAttribute("data-src")); frame.removeAttribute("data-src"); }
1754    if (typeof sheet.showModal === "function") sheet.showModal(); else sheet.setAttribute("open", "");
1755    document.body.classList.add("no-scroll");
1756    var isTouch = window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
1757    if (isTouch && document.activeElement && document.activeElement.blur) document.activeElement.blur();
1758    window.SR_track("sr_signup_sheet_opened", { sheet_context: context || "unknown", page_state: STATE });
1759  }
1760  var sheetClosing = null;
1761  function closeSheet() {
1762    if (!sheet || !sheet.open || sheetClosing) return;
1763    var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1764    function finish() {
1765      sheetClosing = null;
1766      sheet.classList.remove("is-closing");
1767      if (typeof sheet.close === "function") sheet.close(); else sheet.removeAttribute("open");
1768      document.body.classList.remove("no-scroll");
1769    }
1770    if (reduce) { finish(); return; }
1771    sheet.classList.add("is-closing");
1772    sheetClosing = setTimeout(finish, 360);
1773  }
1774  document.addEventListener("click", function (e) {
1775    var open = e.target.closest("[data-open-sheet]");
1776    if (open) { e.preventDefault(); openSheet(open.getAttribute("data-sheet-context")); return; }
1777    if (e.target.closest("[data-close-sheet]")) closeSheet();
1778  });
1779  if (sheet) {
1780    sheet.addEventListener("click", function (e) { if (e.target === sheet) closeSheet(); });
1781    sheet.addEventListener("cancel", function (e) { e.preventDefault(); closeSheet(); });
1782    sheet.addEventListener("close", function () { document.body.classList.remove("no-scroll"); });
1783  }
1784
1785  /* =========================================================
1786     Tour dates — Bandsintown hydration + row behaviour.
1787     Row state:
1788       "presale" → CTA "Pre-sale", opens the sign-up sheet
1789                   (every UK + EU row before general sale)
1790       "onsale"  → CTA "Buy tickets", links to the BIT offer URL
1791       "soldout" → inert
1792       "coming"  → "Coming soon", inert (on sale, but no offer yet)
1793     ========================================================= */
1794  var MONTH_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
1795  var DAY_SHORT = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
1796  var COUNTRY_NAME_TO_ISO = {
1797    "United Kingdom":"GB", "United States":"US", "Ireland":"IE", "Germany":"DE", "France":"FR",
1798    "Italy":"IT", "Spain":"ES", "Portugal":"PT", "Netherlands":"NL", "Belgium":"BE",
1799    "Switzerland":"CH", "Austria":"AT", "Denmark":"DK", "Sweden":"SE", "Norway":"NO",
1800    "Finland":"FI", "Poland":"PL", "Czech Republic":"CZ", "Czechia":"CZ", "Hungary":"HU",
1801    "Greece":"GR", "Luxembourg":"LU", "Croatia":"HR", "Slovenia":"SI", "Slovakia":"SK",
1802    "Romania":"RO", "Bulgaria":"BG", "Serbia":"RS", "Turkey":"TR", "Türkiye":"TR", "Monaco":"MC",
1803    "Australia":"AU", "New Zealand":"NZ", "Canada":"CA", "Mexico":"MX", "Japan":"JP",
1804    "South Africa":"ZA", "United Arab Emirates":"AE"
1805  };
1806  var EU = ["IE","DE","FR","IT","ES","PT","NL","BE","CH","AT","DK","SE","NO","FI","PL","CZ","HU","GR","LU","HR","SI","SK","RO","BG","RS","TR","MC"];
1807  function regionFor(ev) {
1808    var iso = COUNTRY_NAME_TO_ISO[(ev.venue || {}).country || ""] || "";
1809    if (iso === "GB") return "UK";
1810    if (EU.indexOf(iso) !== -1) return "Europe";
1811    return "Rest of world";
1812  }
1813  var REGION_ORDER = ["UK", "Europe", "Rest of world"];
1814
1815  /* EU general sale, per show (client sheet "Simply Red 2027 Europe Tour",
1816     ANNOUNCE tab, 24 Sep). Keyed by BIT event id. "10AM local" is 10:00
1817     CEST = 09:00 BST for everything except Lisbon (WEST = BST). `at` gates
1818     the row; `url` is the promoter's general-sale link, used when BIT has no
1819     offer (API lag, 24 Sep). BIT's own offer wins when present. at: null →
1820     not on sale yet (TBD). Times carry their offset — never parsed as local. */
1821  SR.EU_SALES = {
1822    "108902517": { at: "2026-09-25T10:00:00+01:00", url: "https://simplyred.bol.pt/" }, // Lisbon — 10:00 local = 10:00 BST
1823    "108902527": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.nl/artist/simply-red-tickets/2165" }, // Amsterdam 19 Jun
1824    "108907912": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.nl/artist/simply-red-tickets/2165" }, // Amsterdam 20 Jun
1825    "108907918": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.be/event/613645963?language=en-be&brand=afasdome" }, // Antwerp — client link 24 Sep
1826    "108907927": { at: "2026-09-25T10:00:00+02:00", url: "https://dalessandroegalli.com/events/34552/simply-red/" }, // Venice
1827    "108907952": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=31503&subchannel_id=11911&brand=de_livenation" }, // Munich
1828    "108907957": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=35959&subchannel_id=11911&brand=de_livenation" }, // Nuremberg
1829    "108907960": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=27373&subchannel_id=11911&brand=de_livenation" }, // Fulda
1830    "108907969": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=39685&subchannel_id=11911&brand=de_livenation" }, // Aurich
1831    "108907986": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=38557&subchannel_id=11911&brand=de_livenation" }, // Hemer
1832    "108907996": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=39687&subchannel_id=11911&brand=de_livenation" }, // Hannover
1833    "108908002": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=19391&subchannel_id=11911&brand=de_livenation" }, // Mönchengladbach
1834    "108908114": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=27731&subchannel_id=11911&brand=de_livenation" }, // Bonn
1835    "108908143": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=39197&subchannel_id=11911&brand=de_livenation" }, // Halle (Westf.)
1836    "108908256": { at: "2026-10-09T11:00:00+02:00", url: "https://www.jazzopen-stuttgart.com/" }, // Stuttgart — 9 Oct, 11:00 local
1837    "108908271": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=33199&subchannel_id=11911&brand=de_livenation" }, // Wiesbaden
1838    "108908321": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=31397&subchannel_id=11911&brand=de_livenation" }, // Regensburg
1839    "108908412": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=22571&subchannel_id=11911&brand=de_livenation" }, // Leipzig
1840    "108908455": { at: "2026-09-25T10:00:00+02:00", url: "https://www.oeticket.com/event/simply-red-summer-funk-27-clam-burgarena-22118537/?affiliate=eoe" }, // Klam
1841    "108914691": { at: "2026-09-28T12:00:00+02:00", url: null }, // Locarno — Mon 28 Sep, 12:00 local; no general link yet
1842    "108908466": { at: "2026-09-25T10:00:00+02:00", url: "https://www.ticketmaster.de/artist/2165?venueId=27669&subchannel_id=11911&brand=de_livenation" }, // Füssen
1843    "108914765": { at: "2026-09-25T10:00:00+02:00", url: "https://www.oeticket.com/noapp/event/simply-red-summer-funk-27-viereck-am-fusse-der-burg-hochosterwitz-22137348/?affiliate=H95" }, // Launsdorf (Carinthia) — client link 24 Sep
1844    "108908542": { at: "2026-09-25T10:00:00+02:00", url: "https://www.eventim.hr/artist/simply-red/simply-red-4242148/" }, // Pula
1845    "108914835": { at: null, url: "https://dalessandroegalli.com/events/34552/simply-red/" }, // Rome — on-sale TBD — stays Coming soon until a time is set
1846    "108908549": { at: "2026-09-25T10:00:00+02:00", url: "https://dalessandroegalli.com/events/34552/simply-red/" }, // Pompei
1847    "108909526": { at: "2026-09-25T10:00:00+02:00", url: "https://dalessandroegalli.com/events/34552/simply-red/" }, // Bari
1848    "108908561": { at: "2026-09-25T10:00:00+02:00", url: "https://dalessandroegalli.com/events/34552/simply-red/" } // Lucca
1849  };
1850
1851  // General sale opens for UK rows on the 24th, everything else on the 25th.
1852  function generalSaleOpen(region) {
1853    if (STATE === "onsale") return true;
1854    if (STATE === "onsale-uk") return region === "UK";
1855    return false;
1856  }
1857
1858  function bitStateFor(ev) {
1859    var offer = (ev.offers || []).filter(function (o) { return o.type === "Tickets"; })[0];
1860    if (offer && offer.status === "sold out") return { state: "soldout", url: null };
1861    // EU shows: the client's per-show on-sale time decides, not the page
1862    // state or BIT's zone-less on_sale_datetime.
1863    var sale = SR.EU_SALES[String(ev.id)];
1864    if (sale) {
1865      var at = sale.at ? Date.parse(sale.at) : NaN;
1866      if (isNaN(at) || Date.now() < at) return { state: STATE === "onsale" ? "coming" : "presale", url: null };
1867      if (offer && offer.status === "available") return { state: "onsale", url: offer.url, vendors: ev._vendors || [] };
1868      if (sale.url) return { state: "onsale", url: sale.url, vendors: ev._vendors || [] };
1869      return { state: "coming", url: null };
1870    }
1871    // Before general sale every row registers for the pre-sale, whatever BIT
1872    // says — BIT publishes pre-sale links as status "available", which read
1873    // as general sale (learned the hard way on simpleminds.com, 2026-09-09).
1874    if (!generalSaleOpen(regionFor(ev))) return { state: "presale", url: null };
1875    var onSale = ev.on_sale_datetime ? new Date(ev.on_sale_datetime) : null;
1876    if (onSale && onSale > new Date()) return { state: "coming", url: null };
1877    // The public API can lag the BIT site by a while at on-sale (24 Sep: no
1878    // offers for ~an hour while the site had them). If the on-sale time has
1879    // passed and there's no offer yet, BIT's ticket redirect (/t/<event id>)
1880    // already resolves to the ticket page, so use it. Only for events that
1881    // carry an on_sale_datetime — never guessed for the rest.
1882    if (!offer && onSale && ev.id) return { state: "onsale", url: "https://www.bandsintown.com/t/" + encodeURIComponent(ev.id) + "?app_id=" + encodeURIComponent(SR.BIT_APP_ID || "js_www.simplyred.com"), vendors: ev._vendors || [] };
1883    if (!offer) return { state: "coming", url: null };
1884    if (offer.status === "available") return { state: "onsale", url: offer.url, vendors: ev._vendors || [] };
1885    return { state: "coming", url: null };
1886  }
1887  function ctaTextFor(state) {
1888    if (state === "soldout") return "Sold out";
1889    if (state === "onsale") return "Buy tickets";
1890    if (state === "presale") return "Pre-sale";
1891    return "Coming soon";
1892  }
1893  function esc(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;"); }
1894
1895  function rowHtml(ev) {
1896    // BIT datetime is venue-local with no zone — read the calendar date
1897    // straight off the string so UTC conversion can't shift it a day.
1898    var p = /^(\d{4})-(\d{2})-(\d{2})/.exec(ev.datetime || "");
1899    var d = p ? new Date(+p[1], +p[2] - 1, +p[3]) : new Date(ev.datetime);
1900    var when = DAY_SHORT[d.getDay()] + " " + MONTH_SHORT[d.getMonth()] + " " + d.getDate();
1901    var v = ev.venue || {};
1902    var lineup = (ev.lineup || []).filter(function (a) { return a && a !== SR.BIT_ARTIST; });
1903    // Country after the venue for non-UK rows; UK rows stay city + venue.
1904    var country = v.country === "United Kingdom" ? "" : (v.country || "");
1905    var venueText = (v.name || "Venue TBA") + (country ? ", " + country : "");
1906    var st = bitStateFor(ev);
1907    var attrs = 'data-state="' + st.state + '" data-date="' + esc((ev.datetime || "").slice(0, 10)) + '" data-city="' + esc(v.city || "") + '"';
1908    if (st.url) attrs += ' data-ticket-url="' + esc(st.url) + '"';
1909    if (st.vendors && st.vendors.length > 1) attrs += " data-offers='" + JSON.stringify(st.vendors).replace(/&/g, "&amp;").replace(/'/g, "&#39;") + "'";
1910    return (
1911      '<div class="date-row" data-date-row ' + attrs + '>' +
1912        '<span class="date-when">' + esc(when) + '</span>' +
1913        '<span class="date-where"><span class="date-city">' + esc(v.city || "") + '</span>' +
1914          '<span class="date-venue">' + esc(venueText) + '</span>' +
1915          (lineup.length ? '<span class="date-support">with ' + esc(lineup.join(", ")) + '</span>' : '') +
1916        '</span>' +
1917        '<span class="date-cta">' + esc(ctaTextFor(st.state)) + '</span>' +
1918      '</div>'
1919    );
1920  }
1921
1922  /* Skip festival slots: BIT lists every appearance as an event. */
1923  function isTourDate(ev) {
1924    var title = (ev.title || "").trim().toLowerCase();
1925    return !title || title.indexOf(SR.BIT_ARTIST.toLowerCase()) !== -1;
1926  }
1927
1928  function hydrateDateRows() {
1929    document.querySelectorAll("[data-date-row]").forEach(function (row) {
1930      var state = row.getAttribute("data-state") || "presale";
1931      row.setAttribute("data-state", state);
1932      var cta = row.querySelector(".date-cta");
1933      if (cta) cta.textContent = ctaTextFor(state);
1934      var where = row.querySelector(".date-where");
1935      var venueText = where ? where.textContent.replace(/\s+/g, " ").trim() : "";
1936      var action = { soldout: "Sold out:", onsale: "Buy tickets for", presale: "Register for the pre-sale for", coming: "Tickets coming soon for" }[state];
1937      row.setAttribute("aria-label", action + " " + venueText);
1938      if (state === "onsale" || state === "presale") {
1939        row.setAttribute("role", state === "onsale" ? "link" : "button");
1940        row.setAttribute("tabindex", "0");
1941        row.removeAttribute("aria-disabled");
1942      } else {
1943        row.setAttribute("aria-disabled", "true");
1944        row.setAttribute("role", "group");
1945        row.removeAttribute("tabindex");
1946      }
1947    });
1948  }
1949  document.addEventListener("click", function (e) {
1950    var row = e.target.closest("[data-date-row]");
1951    if (!row) return;
1952    var state = row.getAttribute("data-state");
1953    if (state === "presale") { openSheet("date-row"); return; }
1954    if (state !== "onsale") return;
1955    var vendors = parseVendors(row);
1956    if (vendors.length > 1) { openVendorModal(row, vendors); return; }
1957    var url = row.getAttribute("data-ticket-url");
1958    if (!url) return;
1959    window.SR_track("sr_ticket_clicked", { ticket_city: row.getAttribute("data-city"), ticket_date: row.getAttribute("data-date") });
1960    window.open(url, "_blank", "noopener");
1961  });
1962  document.addEventListener("keydown", function (e) {
1963    if (e.key !== "Enter" && e.key !== " ") return;
1964    var row = e.target.closest("[data-date-row]");
1965    if (!row) return;
1966    e.preventDefault();
1967    row.click();
1968  });
1969
1970  /* "See all dates" — first five rows visible, rest pushed down. */
1971  var datesList = document.querySelector("[data-dates-list]");
1972  var datesToggle = document.querySelector("[data-dates-toggle]");
1973  var datesToggleLabel = document.querySelector("[data-dates-toggle-label]");
1974  var COLLAPSED_ROWS = Infinity;   // client (16 Sep): list every show, no "See all dates"
1975  function applyRowVisibility() {
1976    if (!datesList) return;
1977    var kids = Array.prototype.slice.call(datesList.children);
1978    var rowCount = 0, hidden = [];
1979    kids.forEach(function (el) {
1980      el.classList.remove("is-hidden");
1981      if (el.classList.contains("date-row")) rowCount++;
1982      if (rowCount > COLLAPSED_ROWS || (rowCount === COLLAPSED_ROWS && el.classList.contains("region-divider"))) {
1983        el.classList.add("is-hidden"); hidden.push(el);
1984      }
1985    });
1986    hidden.forEach(function (el, i) { el.style.setProperty("--i", i); });
1987    if (datesToggle) datesToggle.hidden = hidden.length === 0;
1988    datesList.classList.remove("is-expanded");
1989    if (datesToggle) datesToggle.setAttribute("aria-expanded", "false");
1990    if (datesToggleLabel) datesToggleLabel.innerHTML = "See all dates ›";
1991  }
1992  if (datesToggle) datesToggle.addEventListener("click", function () {
1993    var expanded = !datesList.classList.contains("is-expanded");
1994    datesList.classList.toggle("is-expanded", expanded);
1995    datesToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
1996    datesToggleLabel.innerHTML = expanded ? "Show fewer" : "See all dates ›";
1997    var hidden = datesList.querySelectorAll(".is-hidden");
1998    hidden.forEach(function (el, i) { el.style.setProperty("--i", expanded ? i : hidden.length - 1 - i); });
1999    if (!expanded) datesList.scrollIntoView({ behavior: "smooth", block: "start" });
2000    window.SR_track("sr_dates_toggled", { dates_expanded: expanded });
2001  });
2002
2003  function renderEmpty(msg) {
2004    datesList.innerHTML = '<p class="dates-empty">' + esc(msg) + '</p>';
2005    if (datesToggle) datesToggle.hidden = true;
2006  }
2007
2008  /* Vendor list for one V4 event: available ticket offers, one per vendor
2009     (BIT sometimes lists Ticketmaster twice — keep the official one),
2010     official first. */
2011  function vendorsFor(ev) {
2012    var seen = {}, out = [];
2013    (ev.offers || []).forEach(function (o) {
2014      if (!o || o.type !== "Tickets" || o.status !== "available" || !o.url) return;
2015      var key = String(o.vendor_name || "").trim().toLowerCase() || o.url;
2016      var v = { name: o.vendor_name || "", url: o.url, official: !!o.official,
2017                min_price: typeof o.min_price === "number" && o.min_price > 0 ? o.min_price : null, currency: o.currency || null };
2018      if (key in seen) { if (v.official && !out[seen[key]].official) out[seen[key]] = v; return; }
2019      seen[key] = out.length; out.push(v);
2020    });
2021    return out.sort(function (a, b) { return (b.official ? 1 : 0) - (a.official ? 1 : 0); });
2022  }
2023  function parseVendors(row) {
2024    try { var l = JSON.parse(row.getAttribute("data-offers") || "[]"); return Array.isArray(l) ? l.filter(function (o) { return o && o.url; }) : []; }
2025    catch (e) { return []; }
2026  }
2027  // BIT names some outlets by domain: a venue box office ("www.theo2.co.uk")
2028  // or a short link ("bit.ly") for the promoter's own page.
2029  function vendorLabel(o) {
2030    var name = String(o.name || "").trim();
2031    if (!name || /^(bit\.ly|tinyurl\.com|lnk\.to)$/i.test(name)) return { name: "Official tickets", meta: "" };
2032    if (/^(www\.)?[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(name)) return { name: "Venue box office", meta: name.replace(/^www\./i, "") };
2033    return { name: name, meta: "" };
2034  }
2035  function fromPrice(o) {
2036    if (o.min_price === null || !o.currency) return "";
2037    try { return "From " + new Intl.NumberFormat("en-GB", { style: "currency", currency: o.currency }).format(o.min_price).replace(/\.00$/, ""); }
2038    catch (e) { return ""; }
2039  }
2040  var vendorModal = document.querySelector("[data-vendor-modal]");
2041  var vendorList = vendorModal && vendorModal.querySelector("[data-vendor-list]");
2042  var vendorShow = vendorModal && vendorModal.querySelector("[data-vendor-show]");
2043  function openVendorModal(row, vendors) {
2044    if (!vendorModal) { window.open(vendors[0].url, "_blank", "noopener"); return; }
2045    var city = row.querySelector(".date-city"), venue = row.querySelector(".date-venue"), when = row.querySelector(".date-when");
2046    vendorShow.textContent = [city && city.textContent, venue && venue.textContent, when && when.textContent].filter(
2046Boolean).join(" · ");
2047    vendorList.innerHTML = vendors.map(function (o) {
2048      var l = vendorLabel(o);
2049      var meta = [o.official ? "Official" : "", l.meta, fromPrice(o)].filter(Boolean).join(" · ");
2050      return '<li><a class="vendor-link" href="' + esc(o.url) + '" target="_blank" rel="noopener" data-vendor="' + esc(l.name) + '">' +
2051        '<span class="vendor-text"><span class="vendor-name">' + esc(l.name) + '</span>' +
2052        (meta ? '<span class="vendor-meta">' + esc(meta) + '</span>' : '') + '</span>' +
2053        '<svg class="vendor-arrow" viewBox="0 0 18 18" aria-hidden="true" focusable="false"><path d="M4 9h9M9 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
2054        '</a></li>';
2055    }).join("");
2056    vendorModal.dataset.city = row.getAttribute("data-city") || "";
2057    vendorModal.dataset.date = row.getAttribute("data-date") || "";
2058    if (typeof vendorModal.showModal === "function") vendorModal.showModal(); else vendorModal.setAttribute("open", "");
2059    document.body.classList.add("no-scroll");
2060    var isTouch = window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
2061    var first = vendorList.querySelector("a");
2062    if (first && !isTouch) first.focus({ preventScroll: true });
2063    else if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
2064    window.SR_track("sr_vendor_picker_opened", { ticket_city: vendorModal.dataset.city, ticket_date: vendorModal.dataset.date });
2065  }
2066  if (vendorModal) {
2067    var closeVendor = function () { if (typeof vendorModal.close === "function") vendorModal.close(); else vendorModal.removeAttribute("open"); };
2068    vendorModal.addEventListener("click", function (e) {
2069      if (e.target === vendorModal || e.target.closest("[data-close-vendor]")) { closeVendor(); return; }
2070      var link = e.target.closest("a.vendor-link");
2071      if (!link) return;
2072      window.SR_track("sr_ticket_clicked", { ticket_city: vendorModal.dataset.city, ticket_date: vendorModal.dataset.date, ticket_vendor: link.getAttribute("data-vendor") });
2073      setTimeout(closeVendor, 150);
2074    });
2075    vendorModal.addEventListener("close", function () { document.body.classList.remove("no-scroll"); });
2076  }
2077
2078  function hydrateBandsintown() {
2079    if (!datesList) return;
2080    var url = "https://rest.bandsintown.com/artists/" + encodeURIComponent(SR.BIT_ARTIST) +
2081              "/events?app_id=" + encodeURIComponent(SR.BIT_APP_ID) + "&date=upcoming";
2082    // date=upcoming is the API default, but spelling it out matters: on
2083    // 24 Sep the bare query was served from a server-side cache that still
2084    // had no offers 10+ minutes after on-sale, while every other variant
2085    // (date=upcoming, Simply+Red, artist id) was current.
2086    // V4 lists every ticket outlet per show (the endpoint above only gives
2087    // BIT's priority one) — used for the vendor picker only. It has fewer
2088    // offers than the main feed for some EU shows, so it never decides a
2089    // row's state. Optional: failure or >3 s just means no picker.
2090    var v4 = "https://rest.bandsintown.com/V4/artists/id_" + encodeURIComponent(SR.BIT_ARTIST_ID) +
2091             "/events?app_id=" + encodeURIComponent(SR.BIT_APP_ID) + "&date=upcoming";
2092    function getJson(u) { return fetch(u).then(function (r) { return r.ok ? r.json() : null; }); }
2093    var vendorsP = Promise.race([
2094      getJson(v4).catch(function () { return null; }),
2095      new Promise(function (res) { setTimeout(function () { res(null); }, 3000); })
2096    ]);
2097    Promise.all([getJson(url), vendorsP])
2098      .then(function (res) {
2099        var events = res[0], v4events = res[1];
2100        if (!Array.isArray(events)) { renderEmpty("Dates to be announced shortly."); return; }
2101        if (Array.isArray(v4events)) {
2102          var byId = {};
2103          v4events.forEach(function (e) { if (e && e.id) byId[String(e.id)] = vendorsFor(e); });
2104          events.forEach(function (e) { e._vendors = byId[String(e.id)] || []; });
2105        }
2106        renderEvents(events, true);
2107      })
2108      .catch(function () { renderEmpty("Dates to be announced shortly."); });
2109  }
2110
2111  /* Review dataset — Bandsintown-shaped, so it runs through the exact same
2112     grouping / state / render path as the live feed. Venues are plausible
2113     summer-run venues (the Figma's five, plus the UK leg from the working
2114     mobile frame). NOT real dates. ?mock=1 (or ?nolive=1) renders it. */
2115  var SR_MOCK_EVENTS = (function () {
2116    function ev(date, city, country, venue, opts) {
2117      opts = opts || {};
2118      var offers = opts.nooffer ? [] : [{ type: "Tickets", status: opts.soldout ? "sold out" : "available", url: "https://www.bandsintown.com/a/6644" }];
2119      return { datetime: date + "T19:30:00", title: "", venue: { name: venue, city: city, country: country }, offers: offers, lineup: ["Simply Red"].concat(opts.support || []) };
2120    }
2121    return [
2122      ev("2027-06-16", "Lisbon", "Portugal", "MEO Arena"),
2123      ev("2027-06-19", "Amsterdam", "Netherlands", "Ziggo Dome"),
2124      ev("2027-06-20", "Amsterdam", "Netherlands", "Ziggo Dome", { soldout: true }),
2125      ev("2027-06-22", "Antwerp", "Belgium", "AFAS Dome"),
2126      ev("2027-06-25", "Venice", "Italy", "Piazza San Marco"),
2127      ev("2027-06-27", "Berlin", "Germany", "Waldbühne"),
2128      ev("2027-06-29", "Hamburg", "Germany", "Stadtpark"),
2129      ev("2027-07-02", "Paris", "France", "Accor Arena", { nooffer: true }),
2130      ev("2027-07-24", "Chester", "United Kingdom", "Chester Racecourse"),
2131      ev("2027-07-26", "Halifax", "United Kingdom", "The Piece Hall"),
2132      ev("2027-07-27", "Halifax", "United Kingdom", "The Piece Hall"),
2133      ev("2027-07-30", "Scarborough", "United Kingdom", "Open Air Theatre"),
2134      ev("2027-08-01", "Cardiff", "United Kingdom", "Cardiff Castle"),
2135      ev("2027-08-06", "Edinburgh", "United Kingdom", "Edinburgh Castle", { soldout: true }),
2136      ev("2027-08-13", "London", "United Kingdom", "Gunnersbury Park"),
2137      ev("2027-08-14", "Margate", "United Kingdom", "Dreamland"),
2138      // A festival slot, to prove the title filter drops it:
2139      { datetime: "2027-07-10T20:00:00", title: "Isle of Wight Festival 2027", venue: { name: "Seaclose Park", city: "Newport", country: "United Kingdom" }, offers: [], lineup: ["Simply Red"] }
2140    ];
2141  })();
2142
2143  function injectEventSchema(events) {
2144    if (!events.length || document.getElementById("sr-event-schema")) return;
2145    var items = events.slice(0, 30).map(function (ev) {
2146      var v = ev.venue || {};
2147      var offer = (ev.offers || []).filter(function (o) { return o.type === "Tickets"; })[0];
2148      var item = {
2149        "@context": "https://schema.org", "@type": "MusicEvent",
2150        "name": "Simply Red — Summer Funk ’27: " + (v.city || ""),
2151        "startDate": ev.datetime,
2152        "eventStatus": "https://schema.org/EventScheduled",
2153        "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
2154        "location": { "@type": "MusicVenue", "name": v.name || "", "address": { "@type": "PostalAddress", "addressLocality": v.city || "", "addressCountry": v.country || "" } },
2155        "performer": { "@type": "MusicGroup", "name": "Simply Red" },
2156        "url": "https://www.simplyred.com/"
2157      };
2158      if (offer && offer.url) item.offers = { "@type": "Offer", "url": offer.url, "availability": offer.status === "sold out" ? "https://schema.org/SoldOut" : "https://schema.org/InStock" };
2159      return item;
2160    });
2161    var sc = document.createElement("script");
2162    sc.type = "application/ld+json"; sc.id = "sr-event-schema";
2163    sc.textContent = JSON.stringify(items);
2164    document.head.appendChild(sc);
2165  }
2166
2167  function renderEvents(events, isLive) {
2168    events = events.filter(isTourDate);
2169    if (!events.length) { renderEmpty("Dates to be announced shortly."); return; }
2170    events.sort(function (a, b) { return String(a.datetime).localeCompare(String(b.datetime)); });
2171    var byRegion = {};
2172    events.forEach(function (ev) { var r = regionFor(ev); (byRegion[r] = byRegion[r] || []).push(ev); });
2173    var html = "";
2174    REGION_ORDER.forEach(function (region) {
2175      var rows = byRegion[region];
2176      if (!rows || !rows.length) return;
2177      html += '<h3 class="region-divider">' + esc(region) + '</h3>';
2178      rows.forEach(function (ev) { html += rowHtml(ev); });
2179    });
2180    datesList.innerHTML = html;
2181    hydrateDateRows();
2182    applyRowVisibility();
2183    if (isLive) injectEventSchema(events);
2184  }
2185
2186  // Live by default. ?mock=1 renders the review dataset instead.
2187  if (/[?&](mock|nolive)=1/.test(location.search)) renderEvents(SR_MOCK_EVENTS);
2188  else hydrateBandsintown();
2189
2190  /* =========================================================
2191     Tour trailer — only wired once the files exist.
2192     720p to small screens / Save-Data, 1080p otherwise. Plays
2193     muted when scrolled into view, pauses when out.
2194     ========================================================= */
2195  (function () {
2196    var fig = document.querySelector("[data-tour-video]");
2197    var video = document.querySelector("[data-trailer-video]");
2198    var source = document.querySelector("[data-trailer-source]");
2199    var btn = document.querySelector("[data-trailer-mute]");
2200    if (!SR.TRAILER_READY || !fig || !video || !source) {
2201      var pb = document.querySelector("[data-trailer-play]"); if (pb) pb.hidden = true;
2202      return;
2203    }
2204
2205    var small = window.matchMedia && window.matchMedia("(max-width: 768px)").matches;
2206    var saveData = navigator.connection && navigator.connection.saveData;
2207    source.setAttribute("src", (small || saveData) ? video.getAttribute("data-src-small") : video.getAttribute("data-src-large"));
2208    video.hidden = false;
2209    // load() registers the source we just set; with preload="none" it
2210    // fetches nothing — bytes only move when the button is pressed.
2211    video.load();
2212    video.addEventListener("playing", function () { fig.classList.add("is-playing"); if (btn) btn.hidden = false; });
2213    video.addEventListener("error", function () { video.hidden = true; if (btn) btn.hidden = true; fig.classList.remove("is-playing"); }, true);
2214
2215    function safePlay() {
2216      var p = video.play();
2217      if (p && typeof p.catch === "function") p.catch(function () { video.muted = true; video.play().catch(function () {}); });
2218    }
2219    // No autoplay. The centred button starts it with sound; after that,
2220    // scrolling away pauses it and scrolling back resumes it. Tapping the
2221    // playing video pauses / resumes.
2222    var started = false, pausedByScroll = false;
2223    var playBtn = document.querySelector("[data-trailer-play]");
2224    if (playBtn) playBtn.addEventListener("click", function () {
2225      started = true;
2226      video.muted = false;
2227      safePlay();
2228      window.SR_track("sr_trailer_played", { trailer_source: "button" });
2229    });
2230    if ("IntersectionObserver" in window) {
2231      new IntersectionObserver(function (entries) {
2232        entries.forEach(function (entry) {
2233          if (!started) return;
2234          if (entry.isIntersecting) { if (pausedByScroll) { pausedByScroll = false; safePlay(); } }
2235          else if (!video.paused) { pausedByScroll = true; video.pause(); }
2236        });
2237      }, { threshold: 0.2 }).observe(video);
2238    }
2239    video.addEventListener("click", function () { if (!started) return; if (video.paused) safePlay(); else video.pause(); });
2240    video.addEventListener("pause", function () { if (started && !pausedByScroll) fig.classList.add("is-paused"); });
2241    video.addEventListener("play", function () { fig.classList.remove("is-paused"); });
2242
2243    if (btn) {
2244      function sync() {
2245        var unmuted = !video.muted;
2246        btn.setAttribute("aria-pressed", unmuted ? "true" : "false");
2247        btn.setAttribute("aria-label", unmuted ? "Mute trailer" : "Unmute trailer");
2248      }
2249      btn.addEventListener("click", function () {
2250        video.muted = !video.muted;
2251        if (!video.muted && video.paused) safePlay();
2252        sync();
2253        window.SR_track("sr_trailer_unmuted", { trailer_unmuted: !video.muted });
2254      });
2255      video.addEventListener("volumechange", sync);
2256      sync();
2257    }
2258  })();
2259
2260  /* =========================================================
2261     Pull quotes — letter-by-letter red fill locked to scroll.
2262     p = 0 when the quote's top reaches 85% down the viewport,
2263     p = 1 when its bottom reaches 40%. Letters are wrapped in
2264     spans (words kept unbreakable); the CSS does the colour.
2265     ========================================================= */
2266  (function () {
2267    if (!(window.CSS && CSS.supports && CSS.supports("color", "color-mix(in srgb, red 50%, blue)"))) return;
2268    var quotes = Array.prototype.slice.call(document.querySelectorAll(".pull-quote blockquote"));
2269    if (!quotes.length) return;
2270    quotes.forEach(function (bq) {
2271      var p = bq.querySelector("p");
2272      var marks = Array.prototype.slice.call(p.querySelectorAll(".quote-mark"));
2273      marks.forEach(function (m) { m.remove(); });
2274      var text = p.textContent.replace(/\s+/g, " ").trim();
2275      var i = 0, html = "";
2276      text.split(" ").forEach(function (word, wi) {
2277        if (wi) html += " ";
2278        html += '<span class="w">' + Array.prototype.map.call(word, function (ch) {
2279          return '<span class="ch" style="--i:' + (i++) + '">' + ch.replace(/&/g, "&amp;").replace(/</g, "&lt;") + '</span>';
2280        }).join("") + '</span>';
2281      });
2282      p.innerHTML =
2283        '<span class="vh">\u201C' + text.replace(/&/g, "&amp;").replace(/</g, "&lt;") + '\u201D</span>' +
2284        '<span aria-hidden="true">' +
2285          '<span class="qa"><span class="quote-mark quote-mark--open">\u201C</span></span>' + html +
2286          '<span class="qa"><span class="quote-mark quote-mark--close">\u201C</span></span>' +
2287        '</span>';
2288      bq.style.setProperty("--n", i);
2289      bq.classList.add("is-split");
2290    });
2291    var raf = null;
2292    function update() {
2293      raf = null;
2294      var vh = window.innerHeight;
2295      quotes.forEach(function (bq) {
2296        var r = bq.getBoundingClientRect();
2297        var start = vh * 0.85, end = vh * 0.40;
2298        var p = (start - r.top) / (start - end + r.height);
2299        bq.style.setProperty("--p", Math.max(0, Math.min(1, p)).toFixed(4));
2300      });
2301    }
2302    function onScroll() { if (!raf) raf = requestAnimationFrame(update); }
2303    window.addEventListener("scroll", onScroll, { passive: true });
2304    window.addEventListener("resize", onScroll);
2305    update();
2306  })();
2307
2308  /* =========================================================
2309     Newsletter parallax — the background layer drifts at ~15%
2310     of scroll speed while the section crosses the viewport.
2311     ========================================================= */
2312  (function () {
2313    var layer = document.querySelector("[data-parallax]");
2314    if (!layer) return;
2315    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
2316    var section = layer.parentElement, raf = null;
2317    function update() {
2318      raf = null;
2319      var r = section.getBoundingClientRect();
2320      var vh = window.innerHeight;
2321      if (r.bottom < 0 || r.top > vh) return;
2322      // -1 when the section has just entered from below, +1 as it leaves at the top.
2323      var p = ((r.top + r.height / 2) - vh / 2) / (vh / 2 + r.height / 2);
2324      var range = r.height * 0.15;
2325      layer.style.transform = "translate3d(0," + (-p * range).toFixed(1) + "px,0)";
2326    }
2327    function onScroll() { if (!raf) raf = requestAnimationFrame(update); }
2328    window.addEventListener("scroll", onScroll, { passive: true });
2329    window.addEventListener("resize", onScroll);
2330    update();
2331  })();
2332
2333  /* =========================================================
2334     Continue link + cookie banner
2335     ========================================================= */
2336  document.querySelectorAll("[data-continue]").forEach(function (a) {
2337    a.addEventListener("click", function () { window.SR_track("sr_continue_clicked", { page_state: STATE }); });
2338  });
2339  (function () {
2340    var banner = document.querySelector(".cookie-banner");
2341    if (!banner) return;
2342    var accepted = false;
2343    try { accepted = localStorage.getItem("sr_cookie_consent") === "1"; } catch (e) {}
2344    if (accepted) return;
2345    banner.hidden = false;
2346    setTimeout(function () { banner.classList.add("is-visible"); }, 2600);
2347    var accept = banner.querySelector("[data-cookie-accept]");
2348    if (accept) accept.addEventListener("click", function () {
2349      try { localStorage.setItem("sr_cookie_consent", "1"); } catch (e) {}
2350      banner.classList.remove("is-visible");
2351      setTimeout(function () { banner.hidden = true; }, 500);
2352      window.SR_loadGTM();
2353    });
2354  })();
2355</script>
2355
2356</body>
2357</html>

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.