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&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, 'ctl00_btnCatchEnter')"> 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, 'ctl00_search_btnSubmit')"> 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 & 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 & 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 & Complaints</a></li> 572 <li><a href="/yoursu/sustainability/" class="nav_yoursu_sustainability_">Sustainability & 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()">✖</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&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&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, 'ctl00_nonstudentlogin_btnLogin')"> 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'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 & 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> 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 😭 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.