PageSourceSearch

https://keelesu.com/login/

html keelesu.com collected 2026-10-02 19:06:49 UTC 95,314 bytes, 2,297 lines download raw bytes

1
2
3<!DOCTYPE html>
4
5<html lang="en-GB">
6<head><!-- Powered by www.ukmsl.com - v8.90.2609.3001 --><title>
7	Sign In to Your Account
8</title><meta charset="utf-8" /><link href="//static-uk.ukmsl.net/8.90/skins/print.css" rel="stylesheet" type="text/css" media="print" />
9<link href="//static-uk.ukmsl.net/8.90/skins/system.css" rel="stylesheet" type="text/css" media="all" />
10<link href="//static-uk.ukmsl.net/8.90/skins/icons.css" rel="stylesheet" type="text/css" media="all" />
11<link href="//static-uk.ukmsl.net/8.90/skins/Froala.css" rel="stylesheet" type="text/css" media="all" />
12
13<!-- start of su-meta snippet -->
14<!-- Basic Meta Tags -->
15<meta charset="utf-8">
16<meta name="viewport" content="width=device-width, initial-scale=1.0">
17<meta name="referrer" content="strict-origin-when-cross-origin">
18<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin="">
19<link rel="preload" href="/assets/site_resources/Lato-Light.woff2" as="font" type="font/woff2" crossorigin>
20<link rel="preload" href="/assets/site_resources/league-spartan-700.woff2" as="font" type="font/woff2" crossorigin>
21
22<!-- Branding -->
23<meta name="theme-color" content="#22004d">
24<meta name="msapplication-TileColor" content="#22004d">
25<!--
26  Removed msapplication-navbutton-color: Windows Phone IE only, no modern
27  browser reads it (same dead-platform relic as the old UA-sniffing code).
28-->
29
30<!-- iOS / mobile web app -->
31<!-- apple-mobile-web-app-status-bar-style only accepts "default",
32     "black", or "black-translucent" — a hex color is invalid and is
33     silently ignored by iOS. Using "black-translucent" as the closest
34     supported equivalent for a dark theme-color; adjust if you want the
35     plain black bar instead. -->
36<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
37<!-- apple-mobile-web-app-capable is required for standalone/home-screen
38     mode on iOS versions before 16.4; mobile-web-app-capable alone is not
39     enough for those older Safari versions. Keeping both. -->
40<meta name="apple-mobile-web-app-capable" content="yes">
41<meta name="mobile-web-app-capable" content="yes">
42<meta name="apple-itunes-app" content="app-id=1579307063">
43
44<!-- App Titles -->
45<meta name="application-name" content="Keele SU">
46<meta name="apple-mobile-web-app-title" content="Keele SU">
47
48<!-- Icons -->
49<link rel="apple-touch-icon" sizes="180x180" href="https://keelesu.com/stylesheet/Brand/apple-touch-icon.png">
50<link rel="icon" type="image/png" sizes="32x32" href="https://keelesu.com/stylesheet/Brand/favicon-32x32.png">
51<link rel="icon" type="image/png" sizes="16x16" href="https://keelesu.com/stylesheet/Brand/favicon-16x16.png">
52<link rel="manifest" href="https://keelesu.com/stylesheet/Brand/site.webmanifest">
53<link rel="mask-icon" href="https://keelesu.com/stylesheet/Brand/safari-pinned-tab.svg" color="#2e1a47">
54<link rel="shortcut icon" href="https://keelesu.com/stylesheet/Brand/favicon.ico">
55
56<!-- Open Graph (Global defaults via MSL fields) -->
57<meta property="og:type" content="website">
58<meta property="og:site_name" content="Keele SU">
59<meta property="og:locale" content="en_GB">
60<meta property="og:url" content="https://keelesu.com/login/">
61<meta property="og:title" content="Sign In to Your Account">
62<meta property="og:description" content="Login to customise your experience and access additional features">
63<meta property="og:image" content="https://keelesu.com/pageassets/login/Keele-SU-Rich-Blue-Logos_RGB-01_PRIMARY-LOGO-3.png">
64
65<!-- Twitter -->
66<meta name="twitter:card" content="summary_large_image">
67<meta name="twitter:site" content="@keelesu">
68<meta name="twitter:url" content="https://keelesu.com/login/">
69<meta name="twitter:title" content="Sign In to Your Account">
70<meta name="twitter:description" content="Login to customise your experience and access additional features">
71<meta name="twitter:image" content="https://keelesu.com/pageassets/login/Keele-SU-Rich-Blue-Logos_RGB-01_PRIMARY-LOGO-3.png">
72
73<!-- General -->
74<meta name="author" content="Keele SU">
75<meta name="url" content="https://keelesu.com/login/">
76<meta name="title" content="Sign In to Your Account">
77<meta name="description" content="Login to customise your experience and access additional features">
78<meta name="image" content="https://keelesu.com/pageassets/login/Keele-SU-Rich-Blue-Logos_RGB-01_PRIMARY-LOGO-3.png">
79<!--
80  NOTE: <meta name="title"> is not a recognised SEO tag — it does nothing
81  for search engines or browsers. Confirm the CMS also emits a real
82  <title>...</title> element somewhere in this head; none is present in
83  this fragment.
84-->
85
86<!-- Facebook Verify -->
87<meta name="facebook-domain-verification" content="j8zqeuyega64aha524xuxeb1913wri" />
88
89<!--
90  Meta Pixel Code
91  NOTE: this fires unconditionally on every page load. As a UK site,
92  this is subject to UK GDPR/PECR, which requires consent before
93  non-essential tracking pixels/cookies load. If a consent-management
94  layer elsewhere gates this script (e.g. only injecting it after
95  opt-in, or holding it via a tag manager's consent mode), that's fine —
96  but as a standalone unconditional 
96<script> it loads Facebook's
97  tracker before any opt-in choice is made. Verify a consent gate wraps
98  this before shipping.
99-->
100<script>
vendor: 408 bytes, lines 100-109
100
101!function(f,b,e,v,n,t,s)
102{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
103n.callMethod.apply(n,arguments):n.queue.push(arguments)};
104if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
105n.queue=[];t=b.createElement(e);t.async=!0;
106t.src=v;s=b.getElementsByTagName(e)[0];
107s.parentNode.insertBefore(t,s)}(window, document,'script',
108'https://connect.facebook.net/en_GB/fbevents.js');
109fbq('init', '
1092123049768236773
vendor: 32 bytes, lines 109-110
109');
110fbq('track', 'PageView');
111</script>
111
112<noscript><img height="1" width="1" style="display:none"
113src="https://www.facebook.com/tr?id=2123049768236773&ev=PageView&noscript=1"
114/></noscript>
115<!-- End Meta Pixel Code -->
116
117<!-- Structured Data (Unified Organization/EducationalOrganization) -->
118<script type="application/ld+json">
119{
120  "@context": "https://schema.org",
121  "@type": ["Organization", "EducationalOrganization"],
122  "@id": "https://keelesu.com/#organization",
123  "name": "Keele University Students' Union",
124  "alternateName": "Keele SU",
125  "url": "https://keelesu.com/",
126  "logo": "https://keelesu.com/stylesheet/Brand/keelesu_rich_blue_logo_long.png",
127  "sameAs": [
128    "https://www.facebook.com/keelestudentsunion/",
129    "https://www.instagram.com/keelesu/",
130    "https://www.tiktok.com/@keelesu",
131    "https://www.youtube.com/@KeeleSU",
132    "https://www.linkedin.com/company/keele-students-union/",
133    "https://linktr.ee/keelesu"
134  ],
135  "address": {
136    "@type": "PostalAddress",
137    "streetAddress": "Keele University",
138    "addressLocality": "Keele",
139    "addressRegion": "Staffordshire",
140    "postalCode": "ST5 5BJ",
141    "addressCountry": "GB"
142  },
143  "contactPoint": {
144    "@type": "ContactPoint",
145    "telephone": "+44 1782 733700",
146    "email": "[email protected]",
147    "contactType": "Customer Service",
148    "availableLanguage": "English"
149  },
150  "parentOrganization": {
151    "@type": "CollegeOrUniversity",
152    "name": "Keele University",
153    "url": "https://www.keele.ac.uk/"
154  }
155}
156</script>
156
157
158<!-- ========================= -->
159<!-- Structured Data: Consistent Author -->
160<!-- ========================= -->
161<!--
162  This now correctly resolves: the "@id" above matches the "@id" this
163  block references below, so search engines can actually connect the
164  two entities. Previously the first schema had no "@id" at all, so the
165  link was broken.
166-->
167<script type="application/ld+json">
168{
169  "@context": "https://schema.org",
170  "@type": "Organization",
171  "@id": "https://keelesu.com/#author",
172  "name": "Keele SU Team",
173  "url": "https://keelesu.com/yoursu/aboutus/",
174  "parentOrganization": {
175    "@id": "https://keelesu.com/#organization"
176  }
177}
178</script>
178
179
180<!--
181  Canonical link.
182  IMPORTANT: this is still a client-side fallback, kept only for parity
183  with the original snippet. A canonical tag this important should
184  ideally be rendered server-side directly into the HTML <head> by the
185  CMS/MSL template, since crawlers that don't execute JavaScript (and
186  even Googlebot, under rendering-budget constraints) may never see a
187  tag injected this way. Treat this script as a safety net, not the
188  primary mechanism, and prefer wiring MSL's own canonical/URL field
189  into a real <link rel="canonical"> tag if one exists.
190
191  Fixes applied vs the original inline script:
192  - Skips entirely if a canonical <link> already exists in <head>, to
193    avoid emitting a duplicate (which causes Google to ignore or
194    arbitrarily choose between them).
195  - Does NOT force a trailing slash onto paths that already end in a
196    file extension (.aspx, .pdf, .jpg, etc.) — this site's markup
197    (ctl00_basket_lnk... control IDs) indicates ASP.NET/MSL, where many
198    real URLs end in .aspx and would 404 if forced to end in "/".
199  - Uses the built-in URL API instead of ad hoc string/regex surgery to
200    avoid double slashes or malformed protocols.
201  - Deliberately still drops the query string, matching the original
202    behavior. If any pages rely on query strings for distinct content
203    (pagination, filters, search results), those pages need their own
204    canonical logic — this generic script should not run on them.
205-->
206<script>
207(function () {
208  if (document.querySelector('link[rel="canonical"]')) return;
209
210  var url = new URL(window.location.href);
211  var path = url.pathname;
212
213  // Only add a trailing slash if the last path segment has no file
214  // extension (i.e. looks like a directory/route, not a file).
215  var lastSegment = path.split('/').pop();
216  var looksLikeFile = lastSegment.indexOf('.') !== -1;
217
218  if (!looksLikeFile && !path.endsWith('/')) {
219    path += '/';
220  }
221
222  var canonicalURL = url.origin + path;
223
224  var link = document.createElement('link');
225  link.rel = 'canonical';
226  link.href = canonicalURL;
227
228  document.head.appendChild(link);
229})();
230</script>
230
231
232<!-- Google tag (gtag.js) -->
vendor: 65 bytes, lines 232-233
232
233<script async src="https://www.googletagmanager.com/gtag/js?id=
233AW-18398694871
vendor: 13 bytes, line 233
233"></script>
234<script>
235  
vendor: 138 bytes, lines 235-239
235window.dataLayer = window.dataLayer || [];
236  function gtag(){dataLayer.push(arguments);}
237  gtag('js', new Date());
238
239  gtag('config', '
239AW-18398694871
vendor: 5 bytes, line 239
239');
240</script>
240
241
242 <!-- Google Tag Manager -->
243<script>
vendor: 331 bytes, lines 243-247
243(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
244new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
245j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
246'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
247})(window,document,'script','dataLayer','
247GTM-W8L44PJ6
vendor: 3 bytes, line 247
247');
247</script>
247
248<!-- End Google Tag Manager -->
249<!-- end of su-meta snippet -->
250
251
252<!-- start of novus-head snippet -->
253<!-- JavaScript Bundle with Popper -->
254<script defer src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.bundle.min.js" integrity="sha512-HvOjJrdwNpDbkGJIG2ZNqDlVqMo77qbs4Me4cah0HoDrfhrbA+8SBlZn1KrvAQw7cILLPFJvdwIgphzQmMm+Pw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
vendor: 2 bytes, line 254
254
255<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
255
256<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
257
258<!-- Self‑hosted fonts -->
259<style>
260/* Lato Light — Weight 300 */
261@font-face {
262  font-family: "Lato";
263  font-style: normal;
264  font-weight: 300;
265  font-display: swap;
266  src: url("https://keelesu.com/assets/site_resources/Lato-Light.woff2") format("woff2");
267}
268
269/* League Spartan Bold — Weight 700 
270@font-face {
271  font-family: "League Spartan";
272  font-style: normal;
273  font-weight: 700;
274  font-display: swap;
275  src: url("https://keelesu.com/assets/site_resources/league-spartan-700.woff2") format("woff2");
276}
277
278/* League Spartan Bold — Weight 700 */
279@font-face {
280  font-family: "League Spartan";
281  font-style: normal;
282  font-weight: 600;
283  font-display: swap;
284  src: url("https://keelesu.com/assets/site_resources/Spartan-Bold.ttf") format("ttf");
285}
286</style>
287
288
289
290
291<script>
292  (function() {
293    const canonicalLink = document.createElement('link');
294    canonicalLink.rel = 'canonical';
295    canonicalLink.href = 'https://keelesu.com' + window.location.pathname;
296    document.head.appendChild(canonicalLink);
297  })();
298</script>
298
299<!-- end of novus-head snippet -->
300
301<link href="//static-uk.ukmsl.net/8.90/skins/Controls/Breadcrumb.css" rel="stylesheet" type="text/css" media="all" />
302<link href="//static-uk.ukmsl.net/8.90/skins/Controls/BasketLink.css" rel="stylesheet" type="text/css" media="all" />
303<link href="/stylesheet/novus/core.css?v=1410" rel="stylesheet" type="text/css" />
304<link href="/stylesheet/novus/navigation.css?v=380" rel="stylesheet" type="text/css" />
305<link href="/stylesheet/novus/bootstrap.css?v=152" rel="stylesheet" type="text/css" />
306<link href="/pagestylesheet/login/styles.css?v=160" rel="stylesheet" type="text/css" media="all" /><!-- Global site tag (gtag.js) - Google Analytics -->
vendor: 65 bytes, lines 306-307
306
307<script async src="https://www.googletagmanager.com/gtag/js?id=
307G-6L5N1C41XV
vendor: 13 bytes, line 307
307"></script>
308<script>
309	
vendor: 133 bytes, lines 309-312
309window.dataLayer = window.dataLayer || [];
310	function gtag(){dataLayer.push(arguments);}
311	gtag('js', new Date());
312	gtag('config', '
312G-6L5N1C41XV
vendor: 5 bytes, line 312
312');
313</script>
313
314<link href="https://ajax.aspnetcdn.com/ajax/act/16_1_1/Content/AjaxControlToolkit/Styles/Backgrounds.min.css" type="text/css" rel="stylesheet" /></head>
315<body id="msl">
316    <form method="post" action="./" id="aspnetForm">
317<div class="aspNetHidden">
318<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" />
319<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" />
320<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="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" />
321</div>
322
323<script type="text/javascript">
324//<![CDATA[
325var theForm = document.forms['aspnetForm'];
326if (!theForm) {
327    theForm = document.aspnetForm;
328}
329function __doPostBack(eventTarget, eventArgument) {
330    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
331        theForm.__EVENTTARGET.value = eventTarget;
332        theForm.__EVENTARGUMENT.value = eventArgument;
333        theForm.submit();
334    }
335}
336//]]>
337</script>
337
338
339
340<script src="https://ajax.aspnetcdn.com/ajax/4.6/1/WebForms.js" type="text/javascript"></script>
vendor: 2 bytes, line 340
340
341<script type="text/javascript">
342//<![CDATA[
343window.WebForm_PostBackOptions||document.write('
343<script type="text/javascript" src="/WebResource.axd?d=pynGkmcFUV13He1Qd6_TZA5Pm6HoQodZMTRqDRmkRQVgWn7tOgQlDkJJRuri60lphrOTNQ2&amp;t=639190869332169432"><\/script>');//]]>
344</script>
344
345
346
347
348<script type="text/javascript">
349//<![CDATA[
350var __cultureInfo = {"name":"en-GB","numberFormat":{"CurrencyDecimalDigits":2,"CurrencyDecimalSeparator":".","IsReadOnly":true,"CurrencyGroupSizes":[3],"NumberGroupSizes":[3],"PercentGroupSizes":[3],"CurrencyGroupSeparator":",","CurrencySymbol":"£","NaNSymbol":"NaN","CurrencyNegativePattern":1,"NumberNegativePattern":1,"PercentPositivePattern":1,"PercentNegativePattern":1,"NegativeInfinitySymbol":"-∞","NegativeSign":"-","NumberDecimalDigits":2,"NumberDecimalSeparator":".","NumberGroupSeparator":",","CurrencyPositivePattern":0,"PositiveInfinitySymbol":"∞","PositiveSign":"+","PercentDecimalDigits":2,"PercentDecimalSeparator":".","PercentGroupSeparator":",","PercentSymbol":"%","PerMilleSymbol":"‰","NativeDigits":["0","1","2","3","4","5","6","7","8","9"],"DigitSubstitution":1},"dateTimeFormat":{"AMDesignator":"AM","Calendar":{"MinSupportedDateTime":"\/Date(-62135596800000)\/","MaxSupportedDateTime":"\/Date(253402300799999)\/","AlgorithmType":1,"CalendarType":1,"Eras":[1],"TwoDigitYearMax":2049,"IsReadOnly":true},"DateSeparator":"/","FirstDayOfWeek":1,"CalendarWeekRule":2,"FullDateTimePattern":"dd MMMM yyyy HH:mm:ss","LongDatePattern":"dd MMMM yyyy","LongTimePattern":"HH:mm:ss","MonthDayPattern":"d MMMM","PMDesignator":"PM","RFC1123Pattern":"ddd, dd MMM yyyy HH\u0027:\u0027mm\u0027:\u0027ss \u0027GMT\u0027","ShortDatePattern":"dd/MM/yyyy","ShortTimePattern":"HH:mm","SortableDateTimePattern":"yyyy\u0027-\u0027MM\u0027-\u0027dd\u0027T\u0027HH\u0027:\u0027mm\u0027:\u0027ss","TimeSeparator":":","UniversalSortableDateTimePattern":"yyyy\u0027-\u0027MM\u0027-\u0027dd HH\u0027:\u0027mm\u0027:\u0027ss\u0027Z\u0027","YearMonthPattern":"MMMM yyyy","AbbreviatedDayNames":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"ShortestDayNames":["Su","Mo","Tu","We","Th","Fr","Sa"],"DayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"AbbreviatedMonthNames":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec",""],"MonthNames":["January","February","March","April","May","June","July","August","September","October","November","December",""],"IsReadOnly":true,"NativeCalendarName":"Gregorian Calendar","AbbreviatedMonthGenitiveNames":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec",""],"MonthGenitiveNames":["January","February","March","April","May","June","July","August","September","October","November","December",""]},"eras":[1,"A.D.",null,0]};//]]>
351</script>
351
352
353<script src="https://ajax.aspnetcdn.com/ajax/4.6/1/MicrosoftAjax.js" type="text/javascript"></script>
vendor: 2 bytes, line 353
353
354<script type="text/javascript">
355//<![CDATA[
356(window.Sys && Sys._Application && Sys.Observer)||document.write('
356<script type="text/javascript" src="/ScriptResource.axd?d=NJmAwtEo3Ipnlaxl6CMhvl6DVv6mN-f_T9GleXprYSdJat24ORSRp2MgHzKTv7RMDyjpmNp17kiLYmqNfTjOiH9ZwxJObGjuiB_78QzmIFo3Ld4bu-KqIXFacyxI7uv3b6pZYUkoIdYqrIdUk5FZxvfOUcU1&t=ffffffffbec1863d"><\/script>');//]]>
357</script>
357
358
359<script src="https://ajax.aspnetcdn.com/ajax/4.6/1/MicrosoftAjaxWebForms.js" type="text/javascript"></script>
vendor: 2 bytes, line 359
359
360<script type="text/javascript">
361//<![CDATA[
362(window.Sys && Sys.WebForms)||document.write('
362<script type="text/javascript" src="/ScriptResource.axd?d=dwY9oWetJoJoVpgL6Zq8OD0U5mUkqMO8C1rKJavZtjUNmyiUn5yn5Q5faVPjJjfK5g40WTCxLeZxWbrkdd5QwCtfh1f5-qqjMpEsCSjGTZF0ygktRjoYsDEZlkO91nN0JfonM1IFsswLnVxGcfk9jAVQXU01&t=ffffffffbec1863d"><\/script>');//]]>
363</script>
363
364
365<div class="aspNetHidden">
366
367	<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="7CD7556D" />
368	<input type="hidden" name="__EVENTVALIDATION" id="__EVENTVALIDATION" value="qBu9Y2xfcD2MpVxTEYaAJXo7je4+GByuQj+2c4pq/pKY1nK9ZnnT5X6LpUyIZDrrpNYxjvmB+bdkTOzC3mLbXPNUHvanM1qI4FsMaTbL5vMMdR6gx8Tymj0m7QAgHBOoe74z+zAUnZomu+dFNB9rFl1PFktsIcxMeo5JFVIDwCHVpQYoRUEmnc9YnHwyl8tFezAsUdhkryt+1Ro5ZxEdbSyP1QE=" />
369</div>
370
371        
372	
372<script id="ctl00_commonjs" type="text/javascript" src="//static-uk.ukmsl.net/8.90/scripts/common.js"></script>
372
373        
374
375        
376
377        
378
379        <div id="ctl00_pnlCatchEnter" class="CatchEnter" onkeypress="javascript:return WebForm_FireDefaultButton(event, &#39;ctl00_btnCatchEnter&#39;)">
380	
381            <input type="submit" name="ctl00$btnCatchEnter" value="" onclick="javascript:return false;" id="ctl00_btnCatchEnter" style="display:none;" />
382        
383</div>
384
385        
385<script type="text/javascript">
386//<![CDATA[
387Sys.WebForms.PageRequestManager._initialize('ctl00$ctl04', 'aspnetForm', [], [], [], 90, 'ctl00');
388//]]>
389</script>
389
390
391
392        <div id="skin_novus" class="page_login">
393        
394<!-- start of novus-header-redesign snippet -->
395<header class="slim no-padding">
396    <div class="container">
397        <div class="row">
398            <div class="col-sm-4 hidden-sm-down branding no-padding ">
399                <a href="/"><img src="https://keelesu.com/stylesheet/Brand/navbar-logo.png" alt="KeeleSU Logo" URL='/' width="120" height="85"></a>
400                <a class="toggle-left dsk-navigation-toggle" id="navMenu" href="https://keelesu.com/" aria-expanded="false" aria-label="Open menu">
401                    <i class="nav-menu-icon fa-solid fa-bars" aria-hidden="true"></i>
402                    <span class="nav-menu-label">Menu</span>
403                </a>
404
405            </div>
406            <div class="controls col-12 col-md-8 no-padding">
407                <div id="search-form" class="search-form d-none">
408                    
409<div id="search">
410    <div role="search" id="site-search" class="mslwidget">
411	<div id="ctl00_search_pnlSearch" onkeypress="javascript:return WebForm_FireDefaultButton(event, &#39;ctl00_search_btnSubmit&#39;)">
412	
413		<label for="ctl00_search_txtSearchStr" id="ctl00_search_lblSearch" class="msl-visually-hidden">Search</label>
414	    <input name="ctl00$search$txtSearchStr" type="text" id="ctl00_search_txtSearchStr" class="textbox" placeholder="Search text..." />
415	    
416	    <input type="submit" name="ctl00$search$btnSubmit" value="Search" id="ctl00_search_btnSubmit" class="button" />
417	
418</div>
419	</div>
420</div>
421</MSL:search>
422                </div>
423                <div id="login-controls">
424                    <a id="ctl00_ctl31_lnkLogin" class="msl-loginbutton msl-loggedout " href="/login/">Log in</a>
425</MSL:Loginbutton>
426                </div>
427                <div class="admin-links desktop-admin-links">
428                    <ul class="nav-icons icons">
429                        <li>
430                            
431
432<div id="msl-basket-summary">
433    <a title="Review Basket" class="msl-basket-link" href="/shop/basket/">Basket</a>
434	<div class="msl-basket-summary-items">   
435        
436            <div class="msl-basket-itemcount-container">
437                
438                    <span class="msl-basket-noitems-text"></span>
439                
440    		        <div class="msl-basket-itemcount">0</div> 
441                
442            </div>
443        
444        
445
446        
447
448	</div> 
449</div></MSL:BasketLink>
450                            <div class="hidden-xl-down">
451                            </div>
452                        </li>
453                        <!-- CHANGED: was class="fa-solid fa-user"aria-hidden="true" - missing space
454                             meant the class and aria-hidden attributes ran together. Harmless in
455                             most browsers' lenient parsing, but invalid HTML; fixed the spacing. -->
456                        <li class="profile"><a href="https://keelesu.com/profile/"><i class="fa-solid fa-user" aria-hidden="true"></i><span class="hidden-xl-down">My Account</span></a></li>
457                        <li class="admin-container">
458                            <a href="https://keelesu.com/admin/" class="notext" id="admin-trigger"><i class="fas fa-cog" aria-hidden="true"></i><span class="hidden-xl-down">Admin Panel</span></a>
459
460                        </li>
461                        <li> <a class="searchbar" role="button" aria-label="Site Search" href="https://keelesu.com/search/"> <i class="fas fa-search fa-inverse" aria-hidden="true"></i><span class="hidden-xl-down">Site Search</span> </a> </li>
462                    </ul>
463                                                <div class="dropdown-menu">
464                                </MSL:ControlPanel>
465                                </MSL:Admin>
466                            </div>
467                </div>
468            </div>
469        </div>
470    </div>
471    <div class="container mainnav dsk-nav-c">
472        <div class="row">
473            <div class="col-xl-12">
474                </div>
475                  <div class="mobile-admin-links">
476                     <div id="login-controls">
477                    <a id="ctl00_ctl37_lnkLogin" class="msl-loginbutton msl-loggedout " href="/login/">Log in</a>
478</MSL:Loginbutton>
478
479                </div>
480</div>
481                  <div class="admin-links mobile-admin-links">
482                    <ul class="nav-icons icons">
483                        <li>
484                            
485
486<div id="msl-basket-summary">
487    <a title="Review Basket" class="msl-basket-link" href="/shop/basket/">Basket</a>
488	<div class="msl-basket-summary-items">   
489        
490            <div class="msl-basket-itemcount-container">
491                
492                    <span class="msl-basket-noitems-text"></span>
493                
494    		        <div class="msl-basket-itemcount">0</div> 
495                
496            </div>
497        
498        
499
500        
501
502	</div> 
503</div></MSL:BasketLink>
504                            <div class="hidden-xl-down">
505                            </div>
506                        </li>
507                        <!-- CHANGED: was class="fa-solid fa-user"aria-hidden="true" - missing space
508                             meant the class and aria-hidden attributes ran together. Harmless in
509                             most browsers' lenient parsing, but invalid HTML; fixed the spacing. -->
510                        <li class="profile"><a href="https://keelesu.com/profile/"><i class="fa-solid fa-user" aria-hidden="true"></i><span class="hidden-xl-down">My Account</span></a></li>
511                        <li class="admin-container">
512                            <a href="https://keelesu.com/admin/" class="notext" id="admin-trigger"><i class="fas fa-cog" aria-hidden="true"></i><span class="hidden-xl-down">Admin Panel</span></a>
513
514                        </li>
515                        <li> <a class="searchbar" role="button" aria-label="Site Search" href="https://keelesu.com/search/"> <i class="fas fa-search fa-inverse" aria-hidden="true"></i><span class="hidden-xl-down">Site Search</span> </a> </li>
516                    </ul>
517                                                <div class="dropdown-menu">
518                                </MSL:ControlPanel>
519                                </MSL:Admin>
520                            </div>
521                </div>
522                <div aria-label="Main Navigation" id="topnav" class="mslwidget dsk-nav">
523<ul class="msl_navbar">	<li><a href="/whatson/" class="msl-itemhassubnav"><span class="nav-text">Events and Gigs</span></a>
524<ul class="level_1 subnav_nav_whatson_">		<li><a href="https://keelesu.com/gigs/" class="nav_whatson_gigs_">Public Events &amp; Gigs - Open to Everyone</a></li>
525		<li><a href="/whatson/venueinfo/" class="nav_whatson_venueinfo_">Venue Info</a></li>
526</ul>
527</li>
528	<li><a href="/yourunion/" class="msl-itemhassubnav"><span class="nav-text">Student Voice</span></a>
529<ul class="level_1 subnav_nav_yourunion_">		<li><a href="/yourunion/campaigns/" class="nav_yourunion_campaigns_">Campaigns</a></li>
530		<li><a href="/yourunion/elections/" class="nav_yourunion_elections_">Elections</a></li>
531		<li><a href="/yourunion/ideas/" class="nav_yourunion_ideas_">Have Your Say - Your Ideas</a></li>
532		<li><a href="/yourunion/networks/" class="nav_yourunion_networks_">Student Networks</a></li>
533		<li><a href="/yourunion/uniondelegates/" class="nav_yourunion_uniondelegates_">Union Delegates</a></li>
534		<li><a href="/yourunion/voicereps/" class="nav_yourunion_voicereps_">Voice Reps</a></li>
535		<li><a href="/yourunion/electedofficers/" class="nav_yourunion_electedofficers_">Your Current Elected Officers</a></li>
536</ul>
537</li>
538	<li><a href="/advice/" class="msl-itemhassubnav"><span class="nav-text">Advice and Support</span></a>
539<ul class="level_1 subnav_nav_advice_">		<li><a href="/advice/aboutask/" class="nav_advice_aboutask_">About ASK</a></li>
540		<li><a href="/advice/academic/" class="nav_advice_academic_">Academic</a></li>
541		<li><a href="/advice/complaints/" class="nav_advice_complaints_">Complaints</a></li>
542		<li><a href="/advice/discipline/" class="nav_advice_discipline_">Discipline</a></li>
543		<li><a href="/advice/employment/" class="nav_advice_employment_">Employment</a></li>
544		<li><a href="/advice/family/" class="nav_advice_family_">Family</a></li>
545		<li><a href="/advice/graduating/" class="nav_advice_graduating_">Graduate Guide</a></li>
546		<li><a href="/advice/accommodation/" class="nav_advice_accommodation_">Housing</a></li>
547		<li><a href="/advice/international/" class="nav_advice_international_">International Students</a></li>
548		<li><a href="/advice/legal/" class="nav_advice_legal_">Legal</a></li>
549		<li><a href="/advice/finance/" class="nav_advice_finance_">Money</a></li>
550		<li><a href="/advice/wellbeing/" class="nav_advice_wellbeing_">Wellbeing</a></li>
551</ul>
552</li>
553	<li><a href="/activities/" class="msl-itemhassubnav"><span class="nav-text">Get Involved</span></a>
554<ul class="level_1 subnav_nav_activities_">		<li><a href="/activities/clubs-societies/" class="nav_activities_clubs-societies_">Societies</a></li>
555		<li><a href="/activities/au/" class="nav_activities_au_">Sport Clubs</a></li>
556		<li><a href="/activities/volunteering/" class="nav_activities_volunteering_">Volunteering</a></li>
557</ul>
558</li>
559	<li><a href="/shopsandservices/" class="msl-itemhassubnav"><span class="nav-text">Shops and Services</span></a>
560<ul class="level_1 subnav_nav_shopsandservices_">		<li><a href="/shopsandservices/bookinghub/" class="nav_shopsandservices_bookinghub_">Booking Hub</a></li>
561		<li><a href="/shopsandservices/printshop/" class="nav_shopsandservices_printshop_">Keele Print House</a></li>
562		<li><a href="/shopsandservices/vehiclehire/" class="nav_shopsandservices_vehiclehire_">Minibus Bookings</a></li>
563		<li><a href="/shopsandservices/shop/" class="nav_shopsandservices_shop_">Online Shop</a></li>
564		<li><a href="/shopsandservices/student-discounts/" class="nav_shopsandservices_student-discounts_">Student Discounts</a></li>
565		<li><a href="/shopsandservices/venues/" class="nav_shopsandservices_venues_">Venues &amp; Outlets</a></li>
566</ul>
567</li>
568	<li><a href="/yoursu/" class="msl-itemhassubnav"><span class="nav-text">Your Union</span></a>
569<ul class="level_1 subnav_nav_yoursu_">		<li><a href="/yoursu/aboutus/" class="nav_yoursu_aboutus_">About your SU</a></li>
570		<li><a href="/yoursu/contact/" class="nav_yoursu_contact_">Contact Us</a></li>
571		<li><a href="/yoursu/feedback/" class="nav_yoursu_feedback_">Feedback &amp; Complaints</a></li>
572		<li><a href="/yoursu/sustainability/" class="nav_yoursu_sustainability_">Sustainability &amp; Environmental Responsibility</a></li>
573		<li><a href="https://jobs.keelesu.com" class="nav_yoursu_workwithus_">Work with Us</a></li>
574		<li><a href="/yoursu/advertise/" class="nav_yoursu_advertise_">Advertise with us!</a></li>
575</ul>
576</li>
577</ul>
578
579</div></MSL:Navigation>
580              </div>
581            <!--end 12-->
582        </div>
583        <!--end row-->
584    </div>
585</header>
586<div class="adminmenu">
587    <div class="dropdown-menu">
588        </MSL:ControlPanel>
589        </MSL:Admin>
590        <!-- Mobile copy of admin links so MSL links appear inside the nav pop-up -->
591        <div class="admin-links mobile-admin-links">
592            <ul class="nav-icons icons">
593                <li>
594                    
595
596<div id="msl-basket-summary">
597    <a title="Review Basket" class="msl-basket-link" href="/shop/basket/">Basket</a>
598	<div class="msl-basket-summary-items">   
599        
600            <div class="msl-basket-itemcount-container">
601                
602                    <span class="msl-basket-noitems-text"></span>
603                
604    		        <div class="msl-basket-itemcount">0</div> 
605                
606            </div>
607        
608        
609
610        
611
612	</div> 
613</div></MSL:BasketLink>
614                </li>
615                <li class="profile"><a href="https://keelesu.com/profile/"><i class="fa-solid fa-user" aria-hidden="true"></i><span>My Account</span></a></li>
616                <li class="admin-container"><a href="https://keelesu.com/admin/" class="notext" id="admin-trigger-mobile"><i class="fas fa-cog" aria-hidden="true"></i><span>Admin Panel</span></a></li>
617                <li><a class="searchbar" role="button" aria-label="Site Search" href="https://keelesu.com/search/"><i class="fas fa-search fa-inverse" aria-hidden="true"></i><span>Site Search</span></a></li>
618            </ul>
619        </div>
620    </div>
621</div>
622<!-- feedback form and button  -->
623<!--
624<iframe id="feedbackForm" width="640px" height="480px" src="https://forms.office.com/Pages/ResponsePage.aspx?id=7vO7K2i2-U2wY1mJRcPUZnsNe7WejyZOv7GKwMl3GbdUMVVKMDNHNVpPSVJIQVVFT0Q5NjZHRjRLSS4u&embed=true" frameborder="0" marginwidth="0" marginheight="0" style="border: none; max-width:100%; max-height:100vh" allowfullscreen=""> </iframe>
625<div id="FBBS" class="row">
626<div id="fbButton" onclick="formToggle()">How did we do?<br />Give your Feedback here!</div>
627<div id="closeFB" onclick="hideButton()">&#x2716;</div>
628</div>-->
629<script defer>
630  document.addEventListener("DOMContentLoaded", function () {
631    let searchVisible = false;
632    const searchForm = document.getElementById("search-form");
633    const searchBars = document.querySelectorAll("a.searchbar");
634    const navMenu = document.getElementById("navMenu");
635
636    function toggleSearch() {
637      if (!searchForm) return;
638      if (!searchVisible) {
639        searchForm.classList.remove("d-none");
640        searchForm.scrollIntoView({ behavior: "smooth" });
641        searchVisible = true;
642      } else {
643        searchForm.classList.add("d-none");
644        searchVisible = false;
645      }
646    }
647
648    function hideSearch() {
649      if (searchVisible && searchForm) {
650        searchForm.classList.add("d-none");
651        searchVisible = false;
652      }
653    }
654
655    // Keep the mobile search links as real navigation to the search page.
656    // Desktop still opens the inline search form.
657    if (searchBars && searchBars.length) {
658      searchBars.forEach(function (searchBar) {
659        searchBar.addEventListener("click", function (e) {
660          if (window.innerWidth <= 767) return;
661          e.preventDefault();
662          toggleSearch();
663        });
664      });
665    }
666
667    if (navMenu) {
668      const navMenuIcon = navMenu.querySelector(".nav-menu-icon");
669      const navMenuLabel = navMenu.querySelector(".nav-menu-label");
670      const mainNav = document.querySelector(".mainnav");
671
672      function setNavMenuState(isOpen) {
673        navMenu.classList.toggle("is-open", isOpen);
674        navMenu.setAttribute("aria-expanded", String(isOpen));
675        navMenu.setAttribute("aria-label", isOpen ? "Close menu" : "Open menu");
676
677        if (navMenuIcon) {
678          navMenuIcon.classList.remove("fa-bars", "fa-xmark");
679          navMenuIcon.classList.add(isOpen ? "fa-xmark" : "fa-bars");
680        }
681
682        if (navMenuLabel) {
683          navMenuLabel.textContent = isOpen ? "Close" : "Menu";
684        }
685
686        if (mainNav) {
687          mainNav.classList.toggle("open", isOpen);
688          mainNav.style.display = isOpen ? "block" : "none";
689        }
690      }
691
692      setNavMenuState(false);
693
694      navMenu.addEventListener("click", function (e) {
695        e.preventDefault();
696        const isOpen = navMenu.classList.toggle("is-open");
697        setNavMenuState(isOpen);
698        hideSearch();
699        // also close admin menus when opening nav
700        closeAllAdmin();
701      });
702    }
703
704    // Admin menu handling (desktop and mobile)
705    function toggleAdminMenu(trigger) {
706      const container = trigger.closest(".admin-links") || document.querySelector('.adminmenu');
707      if (!container) return;
708      const dropdown = container.querySelector(".dropdown-menu");
709      if (!dropdown) return;
710      const isOpen = dropdown.classList.contains("show");
711      closeAllAdmin();
712      if (!isOpen) {
713        dropdown.classList.add("show");
714      }
715    }
716
717    function closeAllAdmin() {
718      document.querySelectorAll(".dropdown-menu").forEach(function (d) {
719        d.classList.remove("show");
720      });
721    }
722
723    // Attach handlers to admin triggers (covers #admin-trigger, #admin-trigger-mobile and any .admin-container anchors)
724    const adminTriggers = Array.from(document.querySelectorAll("a#admin-trigger, a#admin-trigger-mobile, .admin-container > a"));
725    adminTriggers.forEach(function (t) {
726      t.addEventListener("click", function (e) {
727        e.preventDefault();
728        hideSearch();
729        toggleAdminMenu(t);
730      });
731    });
732
733    // Close menus/search when clicking outside
734    document.addEventListener("click", function (e) {
735      const clickedInsideAdmin = e.target.closest(".admin-links, .adminmenu, a#admin-trigger, a#admin-trigger-mobile");
736      const clickedSearch = e.target.closest("#search-form, a.searchbar");
737      if (!clickedInsideAdmin) {
738        closeAllAdmin();
739      }
740      if (!clickedSearch) {
741        hideSearch();
742      }
743    });
744
745    // Close on Escape key
746    document.addEventListener("keydown", function (e) {
747      if (e.key === "Escape") {
748        hideSearch();
749        closeAllAdmin();
750      }
751    });
752  });
753</script>
753
754
755<script>
756document.addEventListener("DOMContentLoaded", function () {
757    const triggers = document.querySelectorAll(
758        "#admin-trigger, #admin-trigger-mobile"
759    );
760
761    const adminMenu = document.querySelector(".adminmenu");
762    const dropdown = adminMenu?.querySelector(".dropdown-menu");
763
764    if (!adminMenu || !dropdown) return;
765
766    triggers.forEach(trigger => {
767        trigger.addEventListener("click", function (e) {
768            if (window.innerWidth > 767) return;
769
770            e.preventDefault();
771
772            const isHidden =
773                adminMenu.style.display === "none" ||
774                adminMenu.style.display === "";
775
776            if (isHidden) {
777                adminMenu.style.display = "block";
778                dropdown.style.display = "contents";
779            } else {
780                adminMenu.style.display = "none";
781                dropdown.style.display = "none";
782            }
783        });
784    });
785});
786</script>
786
787<!-- end of novus-header-redesign snippet -->
788
789<div class="container-fluid" role="main">
790  <div class="row">
791    <div class="col-xl-12 no-padding">
792	<section class="main">
793		<div class="container no-padding">
794            		<div class="wht-container">
795                           	<div class="row">
796                               		<div class="col-xl-12">
797        						        					<div id="title-section">
798          						<h1><div class="mslwidget">
799Sign In to Your Account
800</div></h1>
801          						</div>
802          						<div class="row breadcrumbs"><div class="span12">
803<nav id="breadcrumb" class="msl-breadcrumb" aria-label="Breadcrumb">
804
805    <ul class="breadcrumb"><li class="current-page" aria-current="page"><span>Sign In to Your Account</span></li></ul>
806
807</nav></div></div>
808          						<div id="pg-description">
809        					<div class="mslwidget">
810Login to customise your experience and access additional features
811</div>
812
813                            		</div>
814                            		</div>
815    				</div>
816				<div class="row">
817                               		<div class="col-xl-12">
818      						
819        <div class="col-xs-12 col-sm-6">
820<div class="loginbox">
821<div class="login-tile login-student"><a href="https://keelesu.com/sso/login.ashx?idp=azure&amp;ReturnUrl=%2f"><img alt="Keele University Logo" src="/pageassets/login/keele-university.jpeg" /></a> <div id="ssologin" class="mslwidget">
822	
823<a id="ctl00_ssologincontrol_hlExtLogin" class="msl-sso-login-link" rel="nofollow" href="/sso/login.ashx?idp=azure&amp;ReturnUrl=%2f">Students & Staff Login</a>
824
825
826</div></div>
827<button class="login-tile login-guest" id="showGandS" onclick="ShowGandS()" type="button"><i class="fa fa-user"></i> Guest Login</button>
828
829<div class="row" id="guestAndStaff">
830<div class="loginbox clearfix"><div class="mslwidget">
831
832<div class="sidepanel">
833
834	<h2>Guest Login</h2>
835	
836	<div id="ctl00_nonstudentlogin_pnlStandardLogin" class="login" onkeypress="javascript:return WebForm_FireDefaultButton(event, &#39;ctl00_nonstudentlogin_btnLogin&#39;)">
837	
838
839		<label for="ctl00_nonstudentlogin_UserName" id="ctl00_nonstudentlogin_lblUserName">Email Address</label>
840		<input name="ctl00$nonstudentlogin$UserName" type="text" id="ctl00_nonstudentlogin_UserName" class="textbox" />
841		<label for="ctl00_nonstudentlogin_Password" id="ctl00_nonstudentlogin_lblPassword">Password</label>
842		<input name="ctl00$nonstudentlogin$Password" type="password" id="ctl00_nonstudentlogin_Password" class="textbox" />
843		
844		<input type="submit" name="ctl00$nonstudentlogin$btnLogin" value="Log In" id="ctl00_nonstudentlogin_btnLogin" class="button" />
845
846		
847
848	
849</div>
850	
851	<div class="login">
852	    <a id="ctl00_nonstudentlogin_lnkRegister" rel="nofollow" href="/account/register/">Create New Account</a>
853	    <a id="ctl00_nonstudentlogin_lnkForgottenPassword" rel="nofollow" href="/account/resetpassword/">Reset Password</a>
854	    
855    </div>
856    
857</div>
858
859</div></div>
860</div>
861
862<p style="text-align:center;"><a href="/account/notmatched">Can&#39;t Login?</a></p>
863</div>
864<script>
865function ShowGandS() {
866    var panel = document.getElementById('guestAndStaff');
867    var button = document.getElementById('showGandS');
868    panel.style.display = 'flex';
869    button.style.display = 'none';
870}
871</script>
871</div>
872
873        
874
875    
876	
877
878        
879        
880        
881					</div>
882       					 <div class="col-xl-12 section-links">
883							</div>
884					</div>
885				</div>
886		</div>
887	</section>
888    </div>
889  </div>
890</div>
891</div>
892
893<!-- start of novus-footer snippet -->
894<footer>
895  <div class="container">
896    <!-- Advertisement Section -->
897    <!-- NOTE: this MSL:Advert control is the "Perks" banner I found on the live homepage
898         (asset/Advert/34/1.png) - it's the one image on the whole page NOT going through
899         MSL's resize pipeline (?thumbnail_width=...&resize_type=CropToFit) that every news/
900         event/officer image uses. That's set by whatever image is uploaded in Adverts Admin
901         for this advert slot, not by this template - re-uploading it there through the same
902         resize pipeline is the actual fix, this file has no control over it. -->
903    <div class="row" style="padding-top: 15px;">
904      <div class="col-12" style="text-align: center;">
905        <div class="mslwidget">
906<a href="/advertclick/33/" target="_blank"><img src="/asset/Advert/33/banner-Blue2-CitizenCard.png" alt="Apply for your Citizen card Today" /></a>
907</div>
908      </div> 
909    </div>
910
911    <!-- Footer Links -->
912    <div class="row f-links">
913      <!-- Useful Links -->
914      <div class="col-sm-4 col-xs-12 f-links-col">
915        <div class="f-links-cont">
916          <div class="f-head">Useful Links</div>
917          <div class="mslwidget footer-nav">
918<ul class="msl_navbar">	<li><a href="/yoursu/workwithus/" target="_blank"><span class="nav-text">Jobs at Keele SU</span></a></li>
919	<li><a href="/yoursu/advertise/"><span class="nav-text">Advertise</span></a></li>
920	<li><a href="/yoursu/feedback/"><span class="nav-text">Make a Complaint</span></a></li>
921	<li><a href="/tac/"><span class="nav-text">Terms &amp; Conditions</span></a></li>
922	<li><a href="/yoursu/aboutus/governance/"><span class="nav-text">Our Policies</span></a></li>
923	<li><a href="/accessibility/"><span class="nav-text">Accessibility</span></a></li>
924	<li><a href="/cookies/"><span class="nav-text">Cookies Policy</span></a></li>
925	<li><a href="/it/"><span class="nav-text">IT Support Links</span></a></li>
926	<li><a href="/accessable/" target="_blank"><span class="nav-text">Accessibility at Keele SU</span></a></li>
927	<li><a href="/system-status/"><span class="nav-text">System Status</span></a></li>
928	<li><a href="/activities/clubs-societies/committeeresources/"><span class="nav-text">Committee Resources</span></a></li>
929	<li><a href="/lostproperty/"><span class="nav-text">Lost Property</span></a></li>
930</ul>
931
932</div>
933        </div>
934      </div>
935
936      <!-- Vision, Mission, and Values -->
937      <div class="col-sm-5 col-xs-12 f-links-col">
938        <div class="f-links-cont">
939          <div class="f-head">Our Vision</div>
940          <p>Empower every student to be who they want to be and make a difference to the world.</p>
941          
942          <div class="f-head">Our Mission</div>
943          <p>Through the delivery of our quality support, activity, representation, entertainment, and value-for-money services, we will ensure that our students feel part of a community which is inclusive, where they can grow in confidence and simply be students.</p>
944          
945          <div class="f-head">Our Values</div>
946          <p>We are Global Citizens, We Challenge & Collaborate, We Unlock Potential.</p>
947        </div>
948      </div>
949
950      <!-- Contact Information and Social Media -->
951      <div class="col-sm-3 col-xs-12 f-links-col">
952        <div class="f-links-cont">
953          <div class="f-head">Contact Us</div>
954          <p>
955            Keele University Students' Union<br>
956            Keele University<br>
957            Newcastle-Under-Lyme<br>
958            Staffordshire<br>
959            ST5 5BJ<br>
960            United Kingdom
961          </p>
962          <p><i class="fas fa-phone"></i> 01782 733700</p>
963          <p><i class="fas fa-envelope"></i> <a href="mailto:[email protected]">[email protected]</a></p>
964          <p><i class="fas fa-map-marker-alt"></i> <a href="https://maps.app.goo.gl/U8Mw4fiQVAKoC1iNA/">Maps & Directions</a></p>
965        </div>
966
967        <!-- Social Media Links -->
968        <br>
969        <div class="f-head">Follow Us!</div>
970        <div id="socialsFooter" class="clearfix">
971          <ul>
972            <!-- Facebook -->
973            <li>
974              <a target="_blank" href="https://www.facebook.com/keelestudentsunion/" aria-label="Facebook" tabindex="0" role="button" aria-pressed="false">
975                <i class="fa-brands fa-facebook"></i>
976                <span class="social-description">Facebook</span>
977              </a>
978            </li>
979            <!-- Instagram -->
980            <li>
981              <a target="_blank" href="https://www.instagram.com/keelesu/" aria-label="Instagram" tabindex="0" role="button" aria-pressed="false">
982                <i class="fa-brands fa-instagram"></i>
983                <span class="social-description">Instagram</span>
984              </a>
985            </li>
986            <!-- YouTube -->
987            <li>
988              <a target="_blank" href="https://www.youtube.com/user/keelesu/" aria-label="YouTube" tabindex="0" role="button" aria-pressed="false">
989                <i class="fa-brands fa-youtube"></i>
990                <span class="social-description">YouTube</span>
991              </a>
992            </li>
993            <!-- LinkedIn -->
994            <li>
995              <a target="_blank" href="https://www.linkedin.com/company/keelesu/" aria-label="LinkedIn" tabindex="0" role="button" aria-pressed="false">
996                <i class="fa-brands fa-linkedin"></i>
997                <span class="social-description">LinkedIn</span>
998              </a>
999            </li>
1000            <!-- TikTok -->
1001            <li>
1002              <a target="_blank" href="https://www.tiktok.com/@keele_su/" aria-label="TikTok" tabindex="0" role="button" aria-pressed="false">
1003                <i class="fa-brands fa-tiktok"></i>
1004                <span class="social-description">TikTok</span>
1005              </a>
1006            </li>
1007            <!-- LinkTree -->
1008            <li>
1009              <a target="_blank" href="https://linktr.ee/keelesu/" aria-label="LinkTree" tabindex="0" role="button" aria-pressed="false">
1010                <!-- CHANGED: added loading="lazy" - this was the one footer image without it -->
1011                <img width="32" height="32" src="https://keelesu.com/stylesheet/Brand/linktree-icon.png" alt="LinkTree" loading="lazy"/>
1012                <span class="social-description">LinkTree</span>
1013              </a>
1014            </li>
1015          </ul>
1016        </div>
1017      </div>
1018    </div>
1019
1020    <!-- Download App Section -->
1021    <div class="row">
1022      <div class="col-12">
1023        <div class="f-head text-center">Download Our App</div>
1024        <p class="text-center">
1025          <a href="https://apps.apple.com/gb/app/studentlink/id1579307063/" rel="noopener noreferrer" target="_blank" aria-label="Keele SU App on the Appstore">
1026            <i class="fa-brands fa-apple fa-2xl"></i>
1027            <span class="social-description">Keele SU iOS App</span>
1028          </a>&nbsp;&nbsp;
1029          <a href="https://play.google.com/store/apps/details?id=com.ukmsl.studentlinkplus.keele" rel="noopener noreferrer" target="_blank" aria-label="Keele SU App on the Playstore">
1030            <i class="fa-brands fa-android fa-2xl"></i>
1031            <span class="social-description">Keele SU Android App</span>
1032          </a>
1033        </p>
1034      </div>
1035    </div>
1036
1037    <!-- Footer Credits -->
1038    <div class="row">
1039      <div class="col-12">
1040        <p class="msl-footer">Keele SU is a Charitable Incorporated Organisation No.1173328</p>
1041        <p class="msl-footer">
1042          <a href="https://www.ukmsl.com/" id="poweredmsl">Powered by MSL</a>
1043        </p>
1044        <p class="msl-footer">
1045          Last Updated: Fri, 28 Aug 2026 11:31:03 GMT
1046        </p>
1047      </div>
1048    </div>
1049  </div>
1050</footer>
1051
1052<!-- Popup for Voting -->
1053<!--class="hide"-->
1054<div id="popup">
1055<MSL:VoterPopup
1056  ContainerTag=""
1057  ContainerID=""
1058  ContainerCssClass=""
1059  SuppressContainer=""
1060  ElectionId="1293"
1061  VotingPeriodNotStartedText=""
1062  VotingNotStartedText="<img src='https://keelesu.com/stylesheet/novus/electionsheaderfs.jpg' alt='Elections Banner' width='100%'>
1063      <br>You haven't voted in the current Elections &#128557
1064      <br><a href='https://keelesu.com/elections/vote/1293/'>Vote Now!</a>
1065      <br>"
1066  VotingInProgressText=""
1067  VotingCompleteText=""
1068  VotingClosedText=""
1069  HideButtonText="Close"
1070/>
1071
1072  <div id="fbmllike">
1073    <div style="clear:both"></div>
1074  </div>
1075</div>
1076
1077<!-- Microsoft Clarity Analytics -->
1078<script>
1079  (function(c, l, a, r, i, t, y) {
1080    c[a] = c[a] || function() { 
1081      (c[a].q = c[a].q || []).push(arguments);
1082    };
1083    t = l.createElement(r);
1084    t.async = true;
1085    t.src = "https://www.clarity.ms/tag/" + i;
1086    y = l.getElementsByTagName(r)[0];
1087    y.parentNode.insertBefore(t, y);
1088  })(window, document, "clarity", "script", "lqptr9hbv4");
1089</script>
1089
1090
1091<!-- Chatbot Styles -->
1092<!-- NOTE: no change needed here. Earlier I flagged this chatbot as a likely big contributor
1093     to page weight, going only off the fact that it existed. Having now seen the actual
1094     implementation: the iframe ships with loading="lazy" AND starts display:none, and the
1095     script below only flips it to display:block after a 60s timer or a manual click. A
1096     hidden, lazy iframe like this is very unlikely to be fetched during a single automated
1097     page-load measurement (the kind carbon calculators run), so this was probably already a
1098     non-issue - correcting my earlier read rather than "fixing" something that isn't broken. -->
1099<style>
1100  :root {
1101    --chat-width: 320px;
1102    --chat-height: 600px;
1103    --chat-margin: 20px;
1104    --chat-button-size: 60px;
1105  }
1106
1107  #chat-popup {
1108    position: fixed;
1109    bottom: var(--chat-margin);
1110    right: calc(var(--chat-margin) * 2);
1111    width: var(--chat-width);
1112    height: var(--chat-height);
1113    display: none;
1114    border: none;
1115    z-index: 1000;
1116  }
1117
1118  #chat-button {
1119    position: fixed;
1120    bottom: var(--chat-margin);
1121    right: var(--chat-margin);
1122    width: var(--chat-button-size);
1123    height: var(--chat-button-size);
1124    border: none;
1125    border-radius: 50%;
1126    background-color: var(--secondary-color, #65b22e);
1127    color: #fff;
1128    cursor: pointer;
1129    z-index: 1001;
1130    font-size: 1rem;
1131  }
1132
1133  .popup-container {
1134    display: none;
1135    position: fixed;
1136    top: 50%;
1137    left: 50%;
1138    transform: translate(-50%, -50%);
1139    background-color: #fff3cd;
1140    color: #856404;
1141    border: 1px solid #ffeeba;
1142    padding: 30px;
1143    border-radius: 10px;
1144    box-shadow: 0 6px 12px rgba(0,0,0,0.15);
1145    max-width: 500px;
1146    width: 90%;
1147    font-family: Arial, sans-serif;
1148    z-index: 1000;
1149    text-align: center;
1150  }
1151
1152  .popup-container h2 {
1153    margin-top: 0;
1154  }
1155
1156  .app-links a {
1157    color: #856404;
1158    text-decoration: underline;
1159    font-weight: bold;
1160  }
1161
1162  .popup-close {
1163    background-color: #856404;
1164    color: white;
1165    border: none;
1166    padding: 10px 16px;
1167    border-radius: 5px;
1168    cursor: pointer;
1169    margin-top: 15px;
1170    font-size: 1em;
1171  }
1172
1173  .popup-close:hover {
1174    background-color: #6c4f03;
1175  }
1176</style>
1177
1178<!-- Chatbot Iframe -->
1179<iframe
1180  id="chat-popup"
1181  src="https://www.chatbase.co/chatbot-iframe/KUL--w-zDPlx88ZgZl8Xh"
1182  title="Live Chat"
1183  loading="lazy"
1184></iframe>
1185
1186<!-- Chat Toggle Button -->
1187<button id="chat-button" type="button" aria-label="Open Chat">Chat</button>
1188
1189<!-- Chat Toggle Script (Vanilla JS) -->
1190<script>
1191  (function() {
1192    'use strict';
1193    
1194    function initChatToggle() {
1195      const chatButton = document.getElementById('chat-button');
1196      const chatPopup = document.getElementById('chat-popup');
1197      
1198      if (!chatButton || !chatPopup) return;
1199
1200      chatButton.addEventListener('click', function(e) {
1201        e.preventDefault();
1202        const isVisible = chatPopup.style.display === 'block';
1203        chatPopup.style.display = isVisible ? 'none' : 'block';
1204      });
1205    }
1206
1207    // Initialize when DOM is ready
1208    if (document.readyState === 'loading') {
1209      document.addEventListener('DOMContentLoaded', initChatToggle);
1210    } else {
1211      initChatToggle();
1212    }
1213  })();
1214</script>
1214
1215
1216<script>
1217window.addEventListener("load", () => {
1218  const path = window.location.pathname;
1219  const isBackend = path.includes("/admin/") || path.includes("/edit/");
1220
1221  const STORAGE_KEY = "chatPopupLastShown";
1222  const COOLDOWN_DAYS = 3;
1223
1224  // Check if popup should show
1225  function canShowPopup() {
1226    const lastShown = localStorage.getItem(STORAGE_KEY);
1227    if (!lastShown) return true;
1228
1229    const now = Date.now();
1230    const diff = now - parseInt(lastShown, 10);
1231    const cooldown = COOLDOWN_DAYS * 24 * 60 * 60 * 1000;
1232
1233    return diff > cooldown;
1234  }
1235
1236  // Save timestamp
1237  function markShown() {
1238    localStorage.setItem(STORAGE_KEY, Date.now().toString());
1239  }
1240
1241  // If admin/edit page → stop
1242  if (isBackend) {
1243    const iframe = document.getElementById("chat-popup");
1244    if (iframe) iframe.style.display = "none";
1245    return;
1246  }
1247
1248  // 🔴 If within 3 days → don't show
1249  if (!canShowPopup()) return;
1250
1251  // ✅ Show popup after 60s
1252  setTimeout(() => {
1253    const interval = setInterval(() => {
1254      const iframe = document.getElementById("chat-popup");
1255      if (iframe) {
1256        iframe.style.display = "block";
1257        iframe.click();
1258
1259        // Save that it has been shown
1260        markShown();
1261
1262        clearInterval(interval);
1263      }
1264    }, 300);
1265  }, 60000);
1266});
1267</script>
1267
1268
1269<!-- Changes Colour/Text on Chatbot Button -->
1270
1271<script>
1272document.addEventListener("DOMContentLoaded", function () {
1273  const chatPopup = document.getElementById("chat-popup");
1274  const chatButton = document.getElementById("chat-button");
1275
1276  if (!chatPopup || !chatButton) return;
1277
1278  // Store original button styles/text so we can restore them
1279  const originalText = chatButton.textContent;
1280  const originalBg = getComputedStyle(chatButton).backgroundColor;
1281  const originalColor = getComputedStyle(chatButton).color;
1282
1283  function updateChatButton() {
1284    const isVisible = getComputedStyle(chatPopup).display === "block";
1285
1286    if (isVisible) {
1287      chatButton.textContent = "Close";
1288      chatButton.style.backgroundColor = "red";
1289      chatButton.style.color = "black";
1290      chatButton.setAttribute("aria-label", "Close Chat");
1291    } else {
1292      chatButton.textContent = originalText || "Chat";
1293      chatButton.style.backgroundColor = originalBg;
1294      chatButton.style.color = originalColor;
1295      chatButton.setAttribute("aria-label", "Open Chat");
1296    }
1297  }
1298
1299  // Watch for changes to the iframe's style attribute
1300  const observer = new MutationObserver(updateChatButton);
1301  observer.observe(chatPopup, {
1302    attributes: true,
1303    attributeFilter: ["style", "class"]
1304  });
1305
1306  // Run once on load
1307  updateChatButton();
1308});
1309
1310</script>
1310
1311
1312<script>
1313// Removes empty location box from events
1314document.querySelectorAll('.location').forEach(location => {
1315    const text = location.textContent.trim();
1316
1317    if (!text) {
1318        location.style.display = 'none';
1319    }
1320});
1321</script>
1321
1322
1323<!-- Cookie Popup -->
1324<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.css" integrity="sha512-LQ97camar/lOliT/MqjcQs5kWgy6Qz/cCRzzRzUCfv0fotsCTC9ZHXaPQmJV8Xu/PVALfJZ7BDezl5lW3/qBxg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
1325<script defer src="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.js" integrity="sha512-yXXqOFjdjHNH1GND+1EO0jbvvebABpzGKD66djnUfiKlYME5HGMUJHoCaeE4D5PTG2YsSJf6dwqyUUvQvS0vaA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
vendor: 2 bytes, line 1325
1325
1326<script>
1327  (function() {
1328    'use strict';
1329    
1330    function initCookieConsent() {
1331      if (typeof window.cookieconsent === 'undefined') return;
1332      
1333      window.cookieconsent.initialise({
1334        palette: {
1335          popup: {
1336            background: "#575952"
1337          },
1338          button: {
1339            background: "#22004d"
1340          }
1341        },
1342        theme: "edgeless",
1343        content: {
1344          message: "This site uses cookies",
1345          dismiss: "Dismiss",
1346          link: "More info",
1347          href: "https://keelesu.com/cookies/"
1348        },
1349        position: "bottom-left"
1350      });
1351    }
1352
1353    window.addEventListener("load", initCookieConsent);
1354  })();
1355</script>
1355
1356
1357<!-- Accessibility Widget -->
1358<script>
1359  window.interdeal = {
1360    sitekey: "6771fa1a11958d581b731f95e0c44d43",
1361    Position: "right",
1362    domains: {
1363      js: "https://cdn.equalweb.com/",
1364      acc: "https://access.equalweb.com/"
1365    },
1366    showTooltip: true,
1367    btnStyle: {
1368      vPosition: ["92%", "20%"],
1369      scale: ["0.5", "0.5"],
1370      color: {
1371        main: "#2b0e48",
1372        second: "#ffffff"
1373      },
1374      icon: {
1375        type: 11,
1376        shape: "rounded"
1377      }
1378    }
1379  };
1380
1381  (function(doc, head, body) {
1382    'use strict';
1383    
1384    const coreCall = doc.createElement('script');
1385    coreCall.src = window.interdeal.domains.js + 'core/4.6.14/accessibility.js';
1386    coreCall.defer = true;
1387    coreCall.integrity = 'sha512-DeZBWLOEHK1CkmU3vQTNSXeENfZmhTHFmn7zzhvZCPERahpp/DTouMc1ZHOu+RAo1FTKnuKEf1NQyB76z8mU1Q==';
1388    coreCall.crossOrigin = 'anonymous';
1389    coreCall.setAttribute('data-cfasync', true);
1390    
1391    (body || head).appendChild(coreCall);
1392  })(document, document.head, document.body);
1393</script>
1393
1394
1395<!-- System Issue Popup (Vanilla JS) -->
1396<script>
1397  (function() {
1398    'use strict';
1399    
1400    function initPopup() {
1401      const popup = document.getElementById('system-issue-popup');
1402      const closeBtn = document.querySelector('.popup-close');
1403      
1404      if (!popup) return;
1405
1406      // Check if popup was already dismissed in this session
1407      const dismissed = sessionStorage.getItem('popupDismissed');
1408      
1409      if (!dismissed) {
1410        setTimeout(function() {
1411          popup.style.display = 'block';
1412        }, 1000);
1413      }
1414
1415      // Close button handler
1416      if (closeBtn) {
1417        closeBtn.addEventListener('click', function() {
1418          popup.style.display = 'none';
1419          sessionStorage.setItem('popupDismissed', 'true');
1420        });
1421      }
1422    }
1423
1424    // Initialize when DOM is ready
1425    if (document.readyState === 'loading') {
1426      document.addEventListener('DOMContentLoaded', initPopup);
1427    } else {
1428      initPopup();
1429    }
1430  })();
1431</script>
1431
1432<!-- end of novus-footer snippet -->
1433
1434<script type="text/javascript">
1435  $(document).ready(function () {
1436    
1437<!-- start of novus-scripts snippet -->
1438// Enhanced Site Script - Optimized Vanilla JavaScript
1439// Performance optimized with caching and efficient DOM operations
1440//
1441// Fixes applied in this version:
1442// 1. Moved the "accessibility cleanup / admin controls / basket buttons /
1443//    event location / referendum" block OUT of the newsItems.forEach loop.
1444//    It previously only ran once per news-scroller item (and not at all
1445//    when there were no news items) instead of once per page load.
1446// 2. Removed no-op `return false` inside a native addEventListener handler
1447//    (that only works with jQuery); replaced with explicit preventDefault.
1448// 3. Added aria-expanded updates to the nav toggle and admin trigger buttons
1449//    so assistive tech can announce expanded/collapsed state.
1450// 4. Added aria-hidden="true" to the accordion chevron icon, and aria-controls
1451//    linking the accordion title to its content panel, for consistency with
1452//    the slider's icons and proper ARIA relationships.
1453// 5. Guarded the popup auto-focus so it only steals focus when the popup is
1454//    actually visible, instead of unconditionally on every page load.
1455// 6. Removed dead/commented-out code (legacy UA sniffing comment left as-is
1456//    where still functionally relevant, but the disabled bxSlider/doubleTap
1457//    leftovers were removed).
1458
1459(function() {
1460  'use strict';
1461
1462  const $ = (selector, context = document) => context.querySelector(selector);
1463
1464  const $$ = (selector, context = document) => {
1465    return Array.from(context.querySelectorAll(selector));
1466  };
1467
1468  // Batch DOM operations
1469  const domBatch = {
1470    operations: [],
1471    add(fn) {
1472      this.operations.push(fn);
1473    },
1474    execute() {
1475      requestAnimationFrame(() => {
1476        this.operations.forEach(fn => fn());
1477        this.operations = [];
1478      });
1479    }
1480  };
1481
1482  // Double Tap To Go (optimized with event delegation where possible)
1483  function doubleTapToGo(selector) {
1484    if (!('ontouchstart' in window) &&
1485        !navigator.msMaxTouchPoints &&
1486        !navigator.userAgent.toLowerCase().match(/windows phone os 7/i)) {
1487      return;
1488    }
1489
1490    let lastTapped = null;
1491    const container = document.getElementById('topnav');
1492    if (!container) return;
1493
1494    container.addEventListener('click', function(e) {
1495      const element = e.target.closest(selector);
1496      if (element && element !== lastTapped) {
1497        e.preventDefault();
1498        lastTapped = element;
1499      }
1500    });
1501
1502    document.addEventListener('click', function(e) {
1503      if (lastTapped && !lastTapped.contains(e.target)) {
1504        lastTapped = null;
1505      }
1506    });
1507
1508    document.addEventListener('touchstart', function(e) {
1509      if (lastTapped && !lastTapped.contains(e.target)) {
1510        lastTapped = null;
1511      }
1512    }, { passive: true });
1513  }
1514
1515  // Relative time formatting (cached results)
1516  const timeCache = new Map();
1517  function relTime(dateString) {
1518    if (timeCache.has(dateString)) {
1519      return timeCache.get(dateString);
1520    }
1521
1522    const date = new Date(dateString);
1523    const now = new Date();
1524    const diff = now - date;
1525    const seconds = Math.floor(diff / 1000);
1526    const minutes = Math.floor(seconds / 60);
1527    const hours = Math.floor(minutes / 60);
1528    const days = Math.floor(hours / 24);
1529
1530    let result;
1531    if (days > 7) result = date.toLocaleDateString();
1532    else if (days > 0) result = `${days} day${days > 1 ? 's' : ''} ago`;
1533    else if (hours > 0) result = `${hours} hour${hours > 1 ? 's' : ''} ago`;
1534    else if (minutes > 0) result = `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
1535    else result = 'Just now';
1536
1537    timeCache.set(dateString, result);
1538    return result;
1539  }
1540
1541  // Main initialization (optimized with fewer DOM queries)
1542  function init() {
1543    const body = document.body;
1544    const mainNav = document.querySelector('.mainnav');
1545
1546    // Remove admin elements for logged out users (single pass)
1547    if (document.querySelector('.msl-loggedout')) {
1548      const adminSelectors = '.admin-links ul li.admin-container, #admin-trigger, .admin-links ul li.profile';
1549      $$(adminSelectors).forEach(el => el.remove());
1550    }
1551
1552    // Event delegation for navigation toggle
1553    body.addEventListener('click', function(e) {
1554      const toggle = e.target.closest('a[class*="toggle-left"]');
1555      if (toggle) {
1556        if (!mainNav.classList.contains('show-nav')) {
1557          body.classList.remove('show-admin');
1558        }
1559        mainNav.classList.toggle('show-nav');
1560        toggle.setAttribute('aria-expanded', mainNav.classList.contains('show-nav'));
1561        return;
1562      }
1563
1564      // Admin trigger
1565      const adminTrigger = e.target.closest('#admin-trigger');
1566      if (adminTrigger) {
1567        e.preventDefault();
1568        if (!body.classList.contains('show-admin')) {
1569          mainNav.classList.remove('show-nav');
1570        }
1571        body.classList.toggle('show-admin');
1572        adminTrigger.setAttribute('aria-expanded', body.classList.contains('show-admin'));
1573        return;
1574      }
1575
1576      // Shop tile clicks
1577      const shopTile = e.target.closest('.shop-cat li');
1578      if (shopTile) {
1579        const link = shopTile.querySelector('a');
1580        if (link) {
1581          e.preventDefault();
1582          window.location = link.href;
1583          return;
1584        }
1585      }
1586
1587      // Accordion clicks
1588      const accordionTitle = e.target.closest('.accordion-title');
1589      if (accordionTitle) {
1590        handleAccordion(accordionTitle);
1591      }
1592
1593      // Submenu clicks
1594      const submenuLink = e.target.closest('#topnav ul.msl_navbar li:has(ul) a');
1595      if (submenuLink) {
1596        const parent = submenuLink.parentElement;
1597        const siblings = Array.from(parent.parentElement.children).filter(el => el !== parent);
1598
1599        siblings.forEach(sib => {
1600          const openUl = sib.querySelector('ul.open');
1601          if (openUl) openUl.classList.remove('open');
1602        });
1603
1604        const firstUl = parent.querySelector('ul');
1605        if (firstUl) firstUl.classList.toggle('open');
1606      }
1607    });
1608
1609    // Enable double-tap navigation
1610    doubleTapToGo('#topnav ul.msl_navbar > li:has(ul) > a');
1611
1612    // Batch DOM manipulations
1613    const exploreTiles = $$('.explore-tiles ul li');
1614    const shopTiles = $$('.shop-cat li');
1615    const tables = $$('#main table:not([class])');
1616    const newsItems = $$('#scroller .news_item');
1617    const mslNewsItems = $$('.msl-news .msl-item');
1618    const bannerItems = $$('#banner .msl-news .msl-item');
1619
1620    // Process explore tiles
1621    exploreTiles.forEach(tile => {
1622      const link = tile.querySelector('a');
1623      if (!link) return;
1624
1625      if (!link.querySelector('.msl-imagenav-image')) {
1626        const img = document.createElement('img');
1627        img.src = 'https://keelesu.com/stylesheet/Brand/image_nav_default.png';
1628        img.alt = 'KeeleSU Branded Image Placeholder';
1629        img.width = 570;
1630        img.height = 285;
1631        img.className = 'msl-imagenav-image';
1632        link.appendChild(img);
1633      }
1634
1635      const desc = tile.querySelectorAll('.msl-imagenav-description, .msl-imagenav-title');
1636      if (desc.length > 0) {
1637        const wrapper = document.createElement('div');
1638        wrapper.className = 'explore_description';
1639        desc[0].parentNode.insertBefore(wrapper, desc[0]);
1640        desc.forEach(el => wrapper.appendChild(el));
1641
1642        const title = wrapper.querySelector('.msl-imagenav-title');
1643        const description = wrapper.querySelector('.msl-imagenav-description');
1644        if (title && description) {
1645          description.parentNode.insertBefore(title, description.nextSibling);
1646        }
1647
1648        const image = tile.querySelector('.msl-imagenav-image');
1649        if (image && wrapper) {
1650          image.parentNode.insertBefore(wrapper, image.nextSibling);
1651        }
1652      }
1653    });
1654
1655    // Process shop tiles
1656    shopTiles.forEach(tile => {
1657      const name = tile.querySelector('.msl-product-name');
1658      const price = tile.querySelector('.msl-product-price');
1659
1660      if (name && price && !name.parentElement.classList.contains('details')) {
1661        const wrapper = document.createElement('div');
1662        wrapper.className = 'details';
1663        name.parentNode.insertBefore(wrapper, name);
1664        wrapper.appendChild(name);
1665        wrapper.appendChild(price);
1666      }
1667    });
1668
1669    // Wrap tables
1670    tables.forEach(table => {
1671      table.className = 'table table-light table-bordered table-striped';
1672      const wrapper = document.createElement('div');
1673      wrapper.className = 'table-responsive';
1674      table.parentNode.insertBefore(wrapper, table);
1675      wrapper.appendChild(table);
1676    });
1677
1678    // Basket handling
1679    $$('.product_box > .btn').forEach(btn => btn.value = 'Add to Basket');
1680
1681    const basket = document.getElementById('basket');
1682    if (basket) {
1683      const basketItems = basket.querySelectorAll('dt');
1684      const itemCount = basketItems.length;
1685      const basketContainer = document.querySelector('.basket-container');
1686
1687      if (itemCount > 0 && basketContainer) {
1688        const basketCount = document.createElement('span');
1689        basketCount.className = 'basket-count';
1690        basketCount.textContent = itemCount;
1691        basketCount.setAttribute('aria-label', `${itemCount} item${itemCount > 1 ? 's' : ''} in basket`);
1692        basketContainer.style.display = '';
1693        basketContainer.classList.add('bagged');
1694        const link = basketContainer.querySelector('a');
1695        if (link) link.appendChild(basketCount);
1696      } else if (basketContainer) {
1697        basketContainer.style.display = 'none';
1698      }
1699
1700      let totalQty = 0;
1701      basket.querySelectorAll('.qty').forEach(qty => {
1702        const match = qty.textContent.match(/\d+/);
1703        if (match) {
1704          const num = parseInt(match[0], 10);
1705          if (!isNaN(num)) totalQty += num;
1706        }
1707      });
1708
1709      if (totalQty > 0) {
1710        const basketIcon = document.querySelector('.basket-icon');
1711        if (basketIcon) {
1712          const quantitySpan = document.createElement('span');
1713          quantitySpan.className = 'basket-quantity';
1714          quantitySpan.setAttribute('aria-label', `${totalQty} item${totalQty > 1 ? 's' : ''} in basket`);
1715          quantitySpan.innerHTML = `<div class="quantity-inner" aria-hidden="true">${totalQty}</div>`;
1716          basketIcon.appendChild(quantitySpan);
1717        }
1718      }
1719
1720      basketItems.forEach(dt => {
1721        const dd = dt.nextElementSibling?.nextElementSibling;
1722        if (dd) {
1723          const wrapper = document.createElement('div');
1724          wrapper.className = 'basket-item';
1725          dt.parentNode.insertBefore(wrapper, dt);
1726          wrapper.appendChild(dt);
1727          wrapper.appendChild(dd);
1728        }
1729      });
1730    }
1731
1732    // Add rel="noopener" to target="_blank" links
1733    $$('a[target="_blank"]').forEach(link => {
1734      const rel = link.getAttribute('rel') || '';
1735      if (!rel.includes('noopener')) {
1736        link.setAttribute('rel', (rel + ' noopener').trim());
1737      }
1738    });
1739
1740    // News scroller formatting
1741    newsItems.forEach(item => {
1742      const title = item.querySelector('h5');
1743      if (title) {
1744        const newTitle = document.createElement('h2');
1745        newTitle.innerHTML = title.innerHTML;
1746        title.replaceWith(newTitle);
1747
1748        const leader = item.querySelector('p.leader');
1749        const newsDetail = document.createElement('div');
1750        newsDetail.className = 'news-detail';
1751        newsDetail.innerHTML = `<div class="news-detail-cell">${newTitle.outerHTML}</div>`;
1752        item.appendChild(newsDetail);
1753
1754        if (leader) {
1755          newsDetail.querySelector('.news-detail-cell').appendChild(leader);
1756        }
1757      }
1758    });
1759
1760    // --- The block below previously lived (incorrectly) inside the
1761    // newsItems.forEach loop above, so it only ran once per news item
1762    // and not at all when #scroller had no news items. It now runs
1763    // exactly once per page load, as intended. ---
1764
1765    // Accessibility cleanup
1766    const popup = document.querySelector('.popup.vp_form');
1767    if (popup) {
1768      popup.setAttribute('tabindex', '0');
1769      popup.setAttribute('title', 'Modal form popup');
1770      // Only steal focus if the popup is actually visible/open, so we don't
1771      // yank focus away from the page on every load when it's hidden.
1772      const isVisible = popup.offsetParent !== null &&
1773        window.getComputedStyle(popup).display !== 'none';
1774      if (isVisible) popup.focus();
1775    }
1776
1777    if (typeof mslUserInfo === 'undefined') {
1778      $$('#skip-to-user, #skip-to-admin, #skip-to-basket').forEach(el => el.remove());
1779    } else {
1780      const skipLogin = document.getElementById('skip-to-login');
1781      if (skipLogin) skipLogin.remove();
1782    }
1783
1784    $$('[aria-label="Site navigation"] > ul > li > a > span').forEach(span => {
1785      const h2 = document.createElement('h2');
1786      span.parentNode.insertBefore(h2, span);
1787      h2.appendChild(span);
1788    });
1789
1790    const adminControls = document.querySelector('.controls-container');
1791    if (adminControls) {
1792      if (adminControls.querySelectorAll('li').length > 0) {
1793        adminControls.style.display = 'block';
1794      } else {
1795        const skipAdmin = document.getElementById('skip-to-admin');
1796        if (skipAdmin) skipAdmin.remove();
1797      }
1798    }
1799
1800    const eventLocation = document.querySelector('.event-location');
1801    if (eventLocation && eventLocation.textContent.length === 1) {
1802      eventLocation.style.display = 'none';
1803    }
1804
1805    $$('.qty > .remove').forEach(btn => btn.textContent = '');
1806
1807    const reviewBtn = document.getElementById('ctl00_basket_lnkReviewBasket');
1808    if (reviewBtn) {
1809      reviewBtn.classList.add('btn', 'btn-primary');
1810    }
1811
1812    const checkoutBtn = document.getElementById('ctl00_basket_lnkCheckout');
1813    if (checkoutBtn) {
1814      checkoutBtn.classList.add('btn', 'btn-secondary');
1815    }
1816
1817    // Format publication metadata for news items
1818    mslNewsItems.forEach(item => {
1819      const title = item.querySelector('.msl-title');
1820      const org = item.querySelector('.msl-organisation');
1821      const byline = item.querySelector('.msl-byline');
1822      const pubDateEl = item.querySelector('.msl-pubdate');
1823
1824      if (pubDateEl) {
1825        const pubDate = pubDateEl.textContent;
1826        const isBanner = item.closest('.mslwidget')?.classList.contains('banner');
1827        pubDateEl.textContent = isBanner ? 'Posted ' + relTime(pubDate) : relTime(pubDate);
1828      }
1829
1830      if (org && byline && title) {
1831        org.textContent += ' | ' + byline.textContent;
1832        title.parentNode.insertBefore(org, title);
1833        byline.remove();
1834      } else if (org && title) {
1835        title.parentNode.insertBefore(org, title);
1836      } else if (byline && title) {
1837        byline.classList.add('msl-organisation');
1838        title.parentNode.insertBefore(byline, title);
1839      }
1840    });
1841
1842    // Banner formatting
1843    bannerItems.forEach(item => {
1844      const h = item.querySelector('h3');
1845      const d = item.querySelector('.msl-pubdate');
1846      const o = item.querySelector('.msl-organisation');
1847
1848      if (h) {
1849        h.classList.remove('msl-title');
1850        const wrapper = document.createElement('div');
1851        wrapper.className = 'msl-title';
1852        h.parentNode.insertBefore(wrapper, h);
1853        wrapper.appendChild(h);
1854
1855        if (d) h.parentNode.insertBefore(d, h.nextSibling);
1856        if (o) h.parentNode.insertBefore(o, h);
1857      }
1858    });
1859
1860    // Hide "More" buttons if no content
1861    const latestEventP = document.querySelector('.banner-latest .latest-event .msl_eventlist > p');
1862    if (latestEventP && latestEventP.textContent.length > 0) {
1863      const btnGroup = document.querySelector('.banner-latest .latest-event .btn-group');
1864      if (btnGroup) btnGroup.style.display = 'none';
1865    }
1866
1867    const noNewsMsg = document.querySelector('.banner-latest .latest-news .msl-no-news-message');
1868    if (noNewsMsg && noNewsMsg.textContent.length > 0) {
1869      const btnGroup = document.querySelector('.banner-latest .latest-news .btn-group');
1870      if (btnGroup) btnGroup.style.display = 'none';
1871    }
1872
1873    // Referendum styling
1874    if (document.querySelector('.referendum')) {
1875      $$('.referendum .vote').forEach(el => el.classList.toggle('btn-group'));
1876      $$('.referendum .votenow').forEach(el => {
1877        el.classList.add('btn', 'btn-primary');
1878      });
1879    }
1880  }
1881
1882  function handleAccordion(title) {
1883    const tabId = title.dataset.tab;
1884    const content = document.getElementById(tabId);
1885    if (!content) return;
1886
1887    // Ensure the ARIA relationship between title and content is present.
1888    if (!title.hasAttribute('aria-controls')) {
1889      title.setAttribute('aria-controls', tabId);
1890    }
1891
1892    const parent = content.parentElement;
1893    const siblings = Array.from(parent.parentElement.children).filter(el => el !== parent);
1894
1895    const isVisible = window.getComputedStyle(content).display !== 'none';
1896    content.style.display = isVisible ? 'none' : 'block';
1897
1898    siblings.forEach(sib => {
1899      const sibContent = sib.querySelector('.accordion-content');
1900      if (sibContent) sibContent.style.display = 'none';
1901    });
1902
1903    title.classList.toggle('active-title');
1904    siblings.forEach(sib => {
1905      const sibTitle = sib.querySelector('.accordion-title');
1906      if (sibTitle) sibTitle.classList.remove('active-title');
1907    });
1908
1909    title.setAttribute('aria-expanded', title.classList.contains('active-title'));
1910
1911    const icon = title.querySelector('i.fa-chevron-down');
1912    if (icon) {
1913      icon.classList.toggle('chevron-top');
1914      icon.setAttribute('aria-hidden', 'true');
1915    }
1916
1917    siblings.forEach(sib => {
1918      const sibIcon = sib.querySelector('.accordion-title i.fa-chevron-down');
1919      if (sibIcon) {
1920        sibIcon.classList.remove('chevron-top');
1921        sibIcon.setAttribute('aria-hidden', 'true');
1922      }
1923    });
1924  }
1925
1926  // Initialize when DOM is ready
1927  if (document.readyState === 'loading') {
1928    document.addEventListener('DOMContentLoaded', init);
1929  } else {
1930    init();
1931  }
1932})();
1933
1934
1935(function() {
1936  function ready(fn) {
1937    if (document.readyState !== 'loading') fn();
1938    else document.addEventListener('DOMContentLoaded', fn);
1939  }
1940
1941  ready(function initAllSliders() {
1942    // Support multiple instances: ID (hero/mini hook) and class (mini)
1943    var containers = Array.prototype.slice.call(
1944      document.querySelectorAll('#sliderControl, .sliderControls-mini')
1945    );
1946
1947    // Remove duplicates if the same element is returned twice
1948    containers = containers.filter(function(el, i, arr){
1949      return arr.indexOf(el) === i;
1950    });
1951
1952    containers.forEach(function(container){
1953      initSliderContainer(container);
1954    });
1955  });
1956
1957  function initSliderContainer(container) {
1958    // Prevent double init
1959    if (container.dataset.sliderInit === '1') return;
1960
1961    // --- STEP 1: Collect slides (news first; else adapt events) ---
1962    var slides = Array.prototype.slice.call(container.querySelectorAll('.news_item'));
1963
1964    if (!slides.length) {
1965      // Try events markup and adapt to news structure
1966      var eventSlides = Array.prototype.slice.call(container.querySelectorAll('.event_item'));
1967      if (eventSlides.length) {
1968        slides = adaptEventsToNews(container, eventSlides);
1969      }
1970    }
1971
1972    if (!slides.length) return; // nothing to do
1973
1974    // --- STEP 2: Track setup ---
1975    var track = container.querySelector('.slider-track');
1976    var createdTrack = false;
1977
1978    if (!track) {
1979      track = document.createElement('div');
1980      track.className = 'slider-track';
1981      createdTrack = true;
1982    }
1983
1984    // Ensure each slide has the expected inner structure
1985    slides.forEach(function(slide, i) {
1986      // If not already structured, build .slide-grid with .slide-image & .slide-text
1987      var hasGrid = slide.querySelector('.slide-grid');
1988      if (!hasGrid) {
1989        var titleBlock = slide.querySelector('h2, h3, h4, h5');
1990        var leader     = slide.querySelector('.leader');
1991        var imageWrap  = slide.querySelector('.news_image');
1992
1993        var grid    = document.createElement('div');
1994        grid.className = 'slide-grid';
1995
1996        var textCol = document.createElement('div');
1997        textCol.className = 'slide-text';
1998
1999        var imageCol = document.createElement('div');
2000        imageCol.className = 'slide-image';
2001
2002        if (imageWrap) {
2003          var img = imageWrap.querySelector('img');
2004          if (img) {
2005            img.removeAttribute('width');
2006            img.removeAttribute('height');
2007            img.loading = 'lazy';
2008          }
2009          imageCol.appendChild(imageWrap);
2010        }
2011
2012        if (titleBlock) textCol.appendChild(titleBlock);
2013        if (leader)     textCol.appendChild(leader);
2014
2015        while (slide.firstChild) slide.removeChild(slide.firstChild);
2016        slide.appendChild(grid);
2017        grid.appendChild(imageCol);
2018        grid.appendChild(textCol);
2019      }
2020
2021      // A11y
2022      slide.setAttribute('role', 'group');
2023      slide.setAttribute('aria-roledescription', 'slide');
2024      slide.setAttribute('aria-label', (i + 1) + ' of ' + slides.length);
2025
2026      // Move into track if we just created it
2027      if (createdTrack) track.appendChild(slide);
2028    });
2029
2030    // Insert track if we created it
2031    if (createdTrack) {
2032      container.insertBefore(track, container.firstChild);
2033    }
2034
2035    // --- STEP 3: Controls & dots (create only if not present) ---
2036    var prevBtn = container.querySelector('.slider-edge.left');
2037    var nextBtn = container.querySelector('.slider-edge.right');
2038
2039    if (!prevBtn) {
2040      prevBtn = document.createElement('button');
2041      prevBtn.type = 'button';
2042      prevBtn.className = 'slider-edge left';
2043      prevBtn.setAttribute('aria-label', 'Previous slide');
2044      prevBtn.innerHTML = '<i class="fa-solid fa-chevron-left" aria-hidden="true"></i>';
2045      container.appendChild(prevBtn);
2046    }
2047
2048    if (!nextBtn) {
2049      nextBtn = document.createElement('button');
2050      nextBtn.type = 'button';
2051      nextBtn.className = 'slider-edge right';
2052      nextBtn.setAttribute('aria-label', 'Next slide');
2053      nextBtn.innerHTML = '<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>';
2054      container.appendChild(nextBtn);
2055    }
2056
2057    var dotsWrap = container.querySelector('.slider-dots');
2058    if (!dotsWrap) {
2059      dotsWrap = document.createElement('div');
2060      dotsWrap.className = 'slider-dots';
2061      dotsWrap.setAttribute('role', 'tablist');
2062      dotsWrap.setAttribute('aria-label', 'Slide navigation');
2063      container.appendChild(dotsWrap);
2064    }
2065
2066    // Pause/Play control so autoplay can be stopped by keyboard/AT users,
2067    // not just via mouseenter.
2068    var pauseBtn = container.querySelector('.slider-pause');
2069    if (!pauseBtn) {
2070      pauseBtn = document.createElement('button');
2071      pauseBtn.type = 'button';
2072      pauseBtn.className = 'slider-pause';
2073      container.appendChild(pauseBtn);
2074    }
2075
2076    container.classList.add('slider-initialized');
2077    container.dataset.sliderInit = '1';
2078
2079    // --- STEP 4: State & behavior ---
2080    var index = 0;
2081    var autoTimer = null;
2082    var AUTO_MS = 6000;
2083    var PAUSE_ON_HOVER = true;
2084    var prefersReducedMotion = window.matchMedia &&
2085      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2086    var userPaused = prefersReducedMotion; // start paused if user prefers reduced motion
2087
2088    // Dots
2089    var dots = [];
2090    function buildDots() {
2091      while (dotsWrap.firstChild) dotsWrap.removeChild(dotsWrap.firstChild);
2092      dots = slides.map(function(_, i) {
2093        var b = document.createElement('button');
2094        b.type = 'button';
2095        b.setAttribute('role', 'tab');
2096        b.setAttribute('aria-label', 'Go to slide ' + (i + 1));
2097        b.addEventListener('click', function() { goTo(i); });
2098        dotsWrap.appendChild(b);
2099        return b;
2100      });
2101    }
2102
2103    function updatePauseButton() {
2104      var isPaused = !autoTimer;
2105      pauseBtn.setAttribute('aria-label', isPaused ? 'Play slideshow' : 'Pause slideshow');
2106      pauseBtn.setAttribute('aria-pressed', String(isPaused));
2107      pauseBtn.innerHTML = isPaused
2108        ? '<i class="fa-solid fa-play" aria-hidden="true"></i>'
2109        : '<i class="fa-solid fa-pause" aria-hidden="true"></i>';
2110    }
2111
2112    function updateUI() {
2113      var slideWidth = slides[0].getBoundingClientRect().width || container.getBoundingClientRect().width;
2114      if (!slideWidth) return; // container hidden? wait for next tick
2115
2116      var offset = index * slideWidth * -1;
2117      track.style.transform = 'translateX(' + offset + 'px)';
2118
2119      dots.forEach(function(d, i) {
2120        d.setAttribute('aria-current', i === index ? 'true' : 'false');
2121      });
2122    }
2123
2124    function goTo(i) {
2125      index = (i + slides.length) % slides.length;
2126      updateUI();
2127      resetAuto();
2128    }
2129    function next() { goTo(index + 1); }
2130    function prev() { goTo(index - 1); }
2131
2132    // Autoplay
2133    function startAuto() {
2134      stopAuto();
2135      if (userPaused) { updatePauseButton(); return; }
2136      autoTimer = setInterval(next, AUTO_MS);
2137      updatePauseButton();
2138    }
2139    function stopAuto()  {
2140      if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
2141      updatePauseButton();
2142    }
2143    function resetAuto() { if (!userPaused) startAuto(); }
2144
2145    pauseBtn.addEventListener('click', function() {
2146      userPaused = !userPaused;
2147      if (userPaused) stopAuto(); else startAuto();
2148    });
2149
2150    // Keyboard
2151    container.setAttribute('tabindex', '0');
2152    container.addEventListener('keydown', function(e) {
2153      var k = e.key;
2154      if (k === 'ArrowRight' || k === 'PageDown') { e.preventDefault(); next(); }
2155      else if (k === 'ArrowLeft' || k === 'PageUp') { e.preventDefault(); prev(); }
2156      else if (k === 'Home') { e.preventDefault(); goTo(0); }
2157      else if (k === 'End')  { e.preventDefault(); goTo(slides.length - 1); }
2158    });
2159
2160    // Buttons
2161    prevBtn.addEventListener('click', prev);
2162    nextBtn.addEventListener('click', next);
2163
2164    // Pause on hover (does not permanently set userPaused, just suspends the timer)
2165    if (PAUSE_ON_HOVER) {
2166      container.addEventListener('mouseenter', function() {
2167        if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
2168      });
2169      container.addEventListener('mouseleave', function() {
2170        if (!userPaused) startAuto();
2171      });
2172    }
2173
2174    // Touch swipe
2175    var startX = null, startY = null, dragging = false;
2176    var SWIPE_THRESHOLD = 40; // px
2177    container.addEventListener('touchstart', function(e) {
2178      var t = e.touches ? e.touches[0] : e;
2179      startX = t.clientX; startY = t.clientY; dragging = true;
2180      if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
2181    }, { passive: true });
2182    container.addEventListener('touchmove', function(e) {
2183      if (!dragging) return;
2184      var t = e.touches ? e.touches[0] : e;
2185      var dx = t.clientX - startX, dy = t.clientY - startY;
2186      if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > SWIPE_THRESHOLD) {
2187        dragging = false; (dx < 0 ? next() : prev());
2188      }
2189    }, { passive: true });
2190    container.addEventListener('touchend', function() {
2191      dragging = false;
2192      if (!userPaused) startAuto();
2193    });
2194
2195    // Resize & late image load sync
2196    var resizeTimer = null;
2197    window.addEventListener('resize', function() {
2198      if (resizeTimer) clearTimeout(resizeTimer);
2199      resizeTimer = setTimeout(updateUI, 150);
2200    });
2201    window.addEventListener('load', updateUI);
2202
2203    // On hidden container at init (e.g., tabs), re-check.
2204    // Also re-check via ResizeObserver where available, so sliders revealed
2205    // later (e.g. inside a tab panel opened after page load) still sync.
2206    setTimeout(updateUI, 200);
2207    setTimeout(updateUI, 500);
2208
2209    if (typeof ResizeObserver !== 'undefined') {
2210      var ro = new ResizeObserver(function() { updateUI(); });
2211      ro.observe(container);
2212    }
2213
2214    // Init
2215    buildDots();
2216    goTo(0);
2217    if (!userPaused) startAuto();
2218    updatePauseButton();
2219  }
2220
2221  /**
2222   * Adapt events markup (.event_item) to the news structure the slider expects.
2223   * Returns the new slides NodeList (now with .news_item).
2224   */
2225  function adaptEventsToNews(container, eventSlides) {
2226    eventSlides.forEach(function(slide) {
2227      // 1) Mark as news_item so existing logic sees it
2228      slide.classList.add('news_item');
2229
2230      // 2) Extract typical events markup
2231      var dl       = slide.querySelector('dl');
2232      var dt       = dl ? dl.querySelector('dt') : null;
2233      var nameLink = dt ? dt.querySelector('.msl_event_name') : null;
2234      var imgWrap  = dt ? dt.querySelector('.msl_event_image') : null;
2235      var desc     = slide.querySelector('.msl_event_description');
2236
2237      // 3) Build grid columns
2238      var grid    = document.createElement('div');
2239      grid.className = 'slide-grid';
2240
2241      var imgCol  = document.createElement('div');
2242      imgCol.className = 'slide-image';
2243
2244      var textCol = document.createElement('div');
2245      textCol.className = 'slide-text';
2246
2247      // Image column: convert to .news_image for CSS compatibility
2248      if (imgWrap) {
2249        imgWrap.classList.add('news_image');
2250        var img = imgWrap.querySelector('img');
2251        if (img) {
2252          img.removeAttribute('width');
2253          img.removeAttribute('height');
2254          img.loading = 'lazy';
2255        }
2256        imgCol.appendChild(imgWrap);
2257      }
2258
2259      // Text column: wrap name in h5, convert description to .leader
2260      if (nameLink) {
2261        var h2 = document.createElement('h2');
2262        h2.appendChild(nameLink);
2263        textCol.appendChild(h2);
2264      }
2265      if (desc) {
2266        desc.classList.add('leader');
2267        textCol.appendChild(desc);
2268      }
2269
2270      // Replace slide content
2271      while (slide.firstChild) slide.removeChild(slide.firstChild);
2272      slide.appendChild(grid);
2273      grid.appendChild(imgCol);
2274      grid.appendChild(textCol);
2275    });
2276
2277    // Return as slides for the rest of the flow
2278    return Array.prototype.slice.call(container.querySelectorAll('.news_item'));
2279  }
2280})();
2281<!-- end of novus-scripts snippet -->
2282
2283  });
2284</script>
2284
2285        </div>
2286        
2287        
2288        
2289        
2290        
2291
2292        
2293
2294    </form>
2295
2296</body>
2297</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.