diff --git a/_collections/_events/ball.md b/_collections/_events/ball.md index d2cb8274..79e4e3a6 100644 --- a/_collections/_events/ball.md +++ b/_collections/_events/ball.md @@ -17,3 +17,7 @@ Plus, thanks to extra funding from the School, we can keep prices low so everybo can get involved. Dress to impress for an unforgettable night! + +Past CSS Balls: [2026](/ball/2026), [2025](/ball/2025), +[2024](/ball/2024), [2023](/ball/2023), [2022](/ball/2022), +[2021](/ball/2021), and [2020](/ball/2020). diff --git a/_config.yml b/_config.yml index e4987e94..1e08d953 100644 --- a/_config.yml +++ b/_config.yml @@ -21,43 +21,26 @@ menu: class: freshers-nav image: /assets/images/freshers-2026/freshers-week-2026-lockup.png image_alt: Freshers Week - - name: News - link: /newsletter - name: Events link: /events - children: - - name: Calendar - link: /calendar - - name: Ball - link: /ball - children: - - name: 2026 - link: /ball/2026 - - name: 2025 - link: /ball/2025 - - name: 2024 - link: /ball/2024 - - name: 2023 - link: /ball/2023 - - name: 2022 - link: /ball/2022 - - name: 2021 - link: /ball/2021 - - name: 2020 - link: /ball/2020 - - name: Merch - link: /merch - - name: Committee - link: /committee + - name: News + link: /newsletter - name: Sponsor link: /sponsor + - name: Committee + link: /committee - name: About - link: /about children: - - name: Membership - link: /membership + - name: Overview + link: /about + - name: Merch + link: /merch - name: Links link: /links + + - name: Join CSS + link: /membership + class: join-nav baseurl: / root: / diff --git a/_includes/header.html b/_includes/header.html index 2228007e..eb6b100a 100644 --- a/_includes/header.html +++ b/_includes/header.html @@ -1,45 +1,49 @@
- +
- - - {% for item in site.menu %} - {% if item.children %} - -
- {% else %} - - {% endif %} - {% endfor %} -
-
- Hamburger menu -
-
- {% for item in site.menu %} - - {% endfor %} -
+ + + +
diff --git a/assets/cross.svg b/assets/cross.svg new file mode 100644 index 00000000..28eb217e --- /dev/null +++ b/assets/cross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/down-arrowhead.svg b/assets/down-arrowhead.svg new file mode 100644 index 00000000..f073eb6c --- /dev/null +++ b/assets/down-arrowhead.svg @@ -0,0 +1,7 @@ + + + diff --git a/css/ball/main.scss b/css/ball/main.scss index 416bd14c..d62341d4 100644 --- a/css/ball/main.scss +++ b/css/ball/main.scss @@ -26,7 +26,11 @@ html { } header { - background-color: $secondary-lowlight; + --nav-background: #79651b; + --nav-panel-background: #79651b; + --nav-hover: #241040; + --nav-accent: #e4d05f; + --nav-font: "Ovo", serif; } body { @@ -119,19 +123,6 @@ body { } } - .dropdown { - font-family: $font-secondary; - } - - .dropdown-content { - background-color: $secondary-lowlight; - - a { - font-family: $font-secondary; - font-size: 20px; - } - } - .flex { display: flex; flex-direction: row; diff --git a/css/events.scss b/css/events.scss index db4f377c..94f487d1 100644 --- a/css/events.scss +++ b/css/events.scss @@ -198,6 +198,29 @@ } } +@media only screen and (min-width: 1201px) { + .event.ball.event-long { + .event-image { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 50%; + height: 100%; + float: none; + } + + .details { + box-sizing: border-box; + width: 50%; + position: static; + max-width: none; + height: auto; + padding-bottom: 5px; + } + } +} + @media only screen and (max-width: 1200px) { .events-listing { grid-template-columns: 1fr; diff --git a/css/freshers.scss b/css/freshers.scss index 534873b0..a1b23b17 100644 --- a/css/freshers.scss +++ b/css/freshers.scss @@ -26,15 +26,6 @@ body { $ink; } -body > header { - position: sticky; - top: 0; - z-index: 100; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(9, 11, 24, 0.86); - backdrop-filter: blur(18px); -} - main { width: 100%; max-width: none; diff --git a/css/main.scss b/css/main.scss index 47a70279..835a729a 100644 --- a/css/main.scss +++ b/css/main.scss @@ -23,6 +23,10 @@ body { flex-direction: column; } +body.mobile-menu-open { + overflow: hidden; +} + .banner { border-radius: 10px; padding: 2px 20px; @@ -253,15 +257,29 @@ main { } header { + --nav-background: #101939; + --nav-panel-background: #101939; + --nav-hover: #2f3c63; + --nav-accent: #cfa8ff; + --nav-text: #fff; + --nav-font: "JetBrains Mono", "Inconsolata", monospace; + + box-sizing: border-box; + position: sticky; + top: 0; + z-index: 2000; display: flex; align-items: center; + min-height: 64px; padding-top: 5px; padding-bottom: 5px; box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2); - background-color: $CSS-Mid-Blue; + background-color: var(--nav-background); .left { + position: relative; + z-index: 2000; flex: 0 0 auto; padding-left: 20px; } @@ -273,77 +291,155 @@ header { flex: 1 1 auto; text-align: right; padding-right: 20px; + + > .navigation-list { + display: flex; + align-items: center; + justify-content: flex-end; + } + } + + .navigation-list { + margin: 0; + padding: 0; + list-style: none; } .nav { position: relative; - // neat animations - &:before, - &:after { + .nav-label { + position: relative; + display: inline-block; + } + + .nav-bracket { position: absolute; - content: ""; - width: 0; - height: 1px; - display: block; - background-color: white; - transition: width 0.2s; + top: 50%; + color: var(--nav-accent); + opacity: 0; + transform: translateY(-50%) scale(0.7); + transition: + opacity 0.12s ease-out, + transform 0.15s ease-out; } - &:before { - left: 0; + .nav-bracket-open { + right: calc(100% + 0.1em); } - &:after { - right: 0; + + .nav-bracket-close { + left: calc(100% + 0.1em); } - &:hover:before, - &:hover:after { - width: 100%; - height: 1px; + a:hover .nav-bracket, + a:focus-visible .nav-bracket, + a:active .nav-bracket, + button:hover .nav-bracket, + button:focus-visible .nav-bracket, + button:active .nav-bracket, + button[aria-expanded="true"] .nav-bracket { + opacity: 1; + transform: translateY(-50%) scale(1); } - &.dropdown:hover .dropdown-content { - display: block; - text-align: center; + &.dropdown.open .dropdown-content, + html:not(.js) &.dropdown:hover .dropdown-content, + html:not(.js) &.dropdown:focus-within .dropdown-content { + visibility: visible; + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0); + transition-delay: 0s; + } + + html:not(.js) &.dropdown .dropdown-content::before { + position: absolute; + right: 0; + bottom: 100%; + left: 0; + height: 10px; + content: ""; } .dropdown-content { - display: none; position: absolute; - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - margin-top: 1px; - padding: 0; - background-color: $CSS-Mid_Blue; - opacity: 1; - z-index: 10; + top: calc(100% + 10px); left: 50%; - transform: translateX(-50%); + z-index: 10; + box-sizing: border-box; + display: grid; + visibility: hidden; + width: 160px; + grid-template-columns: minmax(0, 1fr); + gap: 4px; + margin: 0; + padding: 12px 8px 18px; + list-style: none; + background-color: var(--nav-panel-background); + box-shadow: 0 14px 20px rgba(10, 15, 30, 0.25); + opacity: 0; + pointer-events: none; + transform: translateX(-50%) translateY(-6px); + transition: + opacity 0.12s ease-out, + transform 0.12s ease-out, + visibility 0s linear 0.12s; + + li { + margin: 0; + } a { - display: block; - padding: 10px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 8px 20px; text-decoration: none; text-align: center; + white-space: normal; } - a:hover { - background-color: $TeX-Purple; + + a:hover, + a:focus-visible, + a:active { + color: var(--nav-accent); } - } - &:last-child { - .dropdown-content { - left: auto; - right: 0; - transform: translateX(0.5em); + a:focus-visible { + outline: 2px solid var(--nav-accent); + outline-offset: -2px; } } } + .right > .navigation-list > .nav { + margin: 0; + + > a, + > button { + box-sizing: border-box; + display: flex; + align-items: center; + min-height: 44px; + padding: 8px 12px; + } + } + + .right .nav-bracket-open { + right: 100%; + } + + .right .nav-bracket-close { + left: 100%; + } + .logo { display: block; - width: 100px; - height: 50px; + width: 80px; + height: 40px; img { width: 100%; @@ -353,98 +449,219 @@ header { } .hamburger { - flex: 1 1 auto; + position: relative; + flex: 0 0 auto; + margin-left: auto; text-align: right; display: none; - padding-right: 20px; + padding: 10px 20px 10px 10px; + border: 0; + color: white; + background: none; + cursor: pointer; z-index: 2000; - padding-top: 5px; + + &:focus-visible { + outline: 3px solid white; + outline-offset: 2px; + } + + .hamburger-icon { + display: block; + width: 32px; + height: 32px; + transform-origin: center; + transition: + opacity 0.15s ease-out, + transform 0.18s ease-out; + } + + .hamburger-icon-open { + opacity: 1; + transform: rotate(0) scale(1); + } + + .hamburger-icon-close { + position: absolute; + top: 10px; + left: 10px; + opacity: 0; + transform: rotate(-90deg) scale(0.65); + } + + &[aria-expanded="true"] { + .hamburger-icon-open { + opacity: 0; + transform: rotate(90deg) scale(0.65); + } + + .hamburger-icon-close { + opacity: 1; + transform: rotate(0) scale(1); + } + } } .mobile-menu { - font-family: "JetBrains Mono", "Inconsolata", monospace; + font-family: var(--nav-font); position: fixed; - top: 0; + top: 64px; left: 0; width: 100vw; - height: 100vh; - opacity: 0; + height: calc(100vh - 64px); + height: calc(100dvh - 64px); display: flex; visibility: hidden; + overflow-y: auto; + overscroll-behavior: contain; - padding-top: 100px; - - background-color: $CSS-Deep-Blue; + background-color: var(--nav-panel-background); z-index: 1000; flex-direction: column; - align-items: flex-end; + align-items: flex-start; - transition: opacity 0.2s; + transform: translateY(-100%); + transition: + transform 0.18s ease-out, + visibility 0s linear 0.18s; + will-change: transform; &.visible { visibility: visible; - opacity: 1; + transform: translateY(0); + transition-delay: 0s; + } + + > .navigation-list { + display: flex; + width: 100%; + flex-direction: column; + align-items: flex-start; + } + + .mobile-dropdown { + width: 100%; + margin: 0; + padding: 0; + list-style: none; } .nav { - text-align: right; - font-size: 2em; - line-height: 1.6em; - margin-left: 40px; - margin-right: 20px; + text-align: left; + font-size: 30px; + line-height: 1.2; + width: 100%; + margin: 0; - a { - &:active, - &:focus { - outline: none; - } - cursor: pointer; + .nav-bracket { + font-size: 0.8em; } - &:before { - display: none; + .dropdown-chevron { + width: 0.42em; } - &:first-child:before, - &:after { - position: relative; - content: ""; - display: block; - width: 80vw; - height: 2px; - background-color: white; + + > a, + > button, + .mobile-dropdown a { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 66px; + padding: 10px 16px 10px 24px; + transition: background-color 0.15s; + } + + .mobile-dropdown a { + padding-left: 48px; + } + + > a:hover, + > a:focus-visible, + > a:active, + > button:hover, + > button:focus-visible, + > button:active, + > button[aria-expanded="true"], + .mobile-dropdown a:hover, + .mobile-dropdown a:focus-visible, + .mobile-dropdown a:active { + background-color: var(--nav-hover); + } + + a, + button { + &:focus-visible { + outline: 3px solid white; + outline-offset: 2px; + } + } + + a { + cursor: pointer; } } } & .nav { display: inline-block; - font-size: 1.5em; - font-family: "JetBrains Mono", "Inconsolata", monospace; - font-weight: bold; + font-size: 1.25rem; + font-family: var(--nav-font); + font-weight: 700; margin-left: 10px; margin-right: 10px; a { padding: none; - color: white; + color: var(--nav-text); + font-family: var(--nav-font); text-decoration: none; } button { - color: white; + display: inline-flex; + align-items: center; + color: var(--nav-text); background: none; padding: 0; border: none; text-decoration: none; - font-weight: bold; + font-family: var(--nav-font); + font-weight: 700; + cursor: default; + + .dropdown-chevron { + display: inline-block; + width: 0.5em; + height: 1em; + margin-left: 0.25em; + object-fit: contain; + transform-origin: center; + vertical-align: middle; + } + + &[aria-expanded="true"] .dropdown-chevron { + transform: rotate(180deg); + } } } } @media only screen and (max-width: 1050px) { header { + &:before { + position: absolute; + inset: 0; + z-index: 1500; + background-color: inherit; + content: ""; + pointer-events: none; + } + .left { padding-left: 10px; } @@ -461,6 +678,15 @@ header { } } +@media (prefers-reduced-motion: reduce) { + header .mobile-menu, + header .hamburger-icon, + header .nav-bracket, + header .dropdown-content { + transition: none; + } +} + footer { color: white; font-family: "JetBrains Mono", "Inconsolata", monospace; @@ -492,16 +718,37 @@ header .nav.freshers-nav .nav-item-image { } header .mobile-menu .nav.freshers-nav .nav-item-image { - height: 74px; + height: 44px; } header .mobile-menu .nav.freshers-nav > a { display: flex; - align-items: flex-start; - justify-content: flex-end; + align-items: center; + justify-content: flex-start; width: 100%; } +header .nav.join-nav { + > a { + white-space: nowrap; + } + + .nav-label { + &:after { + position: absolute; + right: 0; + bottom: -0.2em; + left: 0; + height: 0.16em; + border-radius: 999px; + background-color: var(--nav-accent); + content: ""; + pointer-events: none; + transform: rotate(-1.5deg); + } + } +} + hr { color: #9d68dc; } diff --git a/js/hamburger.js b/js/hamburger.js index 612787b5..a848cb63 100644 --- a/js/hamburger.js +++ b/js/hamburger.js @@ -1,23 +1,175 @@ +document.documentElement.classList.add("js"); + document.addEventListener("DOMContentLoaded", () => { - let hamburger = document.getElementById("hamburger"); - let menu = document.getElementById("hamburger-menu"); + const hamburger = document.getElementById("hamburger"); + const menu = document.getElementById("hamburger-menu"); + const desktopDropdowns = document.querySelectorAll("header .right .dropdown"); + const desktopCloseTimers = new WeakMap(); + + const clearDesktopCloseTimer = (dropdown) => { + const timer = desktopCloseTimers.get(dropdown); + + if (timer) { + window.clearTimeout(timer); + desktopCloseTimers.delete(dropdown); + } + }; + + const updateDesktopDropdown = (dropdown, isExpanded, openedBy = "") => { + if (!isExpanded) clearDesktopCloseTimer(dropdown); + + const trigger = dropdown.querySelector(":scope > button"); + trigger?.setAttribute("aria-expanded", String(isExpanded)); + dropdown.classList.toggle("open", isExpanded); + dropdown.dataset.openedBy = isExpanded ? openedBy : ""; + }; + + const setDesktopDropdownExpanded = ( + dropdown, + isExpanded, + openedBy = "activation", + ) => { + if (isExpanded) { + desktopDropdowns.forEach((otherDropdown) => { + if (otherDropdown !== dropdown) { + updateDesktopDropdown(otherDropdown, false); + } + }); + } + + updateDesktopDropdown(dropdown, isExpanded, openedBy); + }; + + desktopDropdowns.forEach((dropdown) => { + const trigger = dropdown.querySelector(":scope > button"); + let pointerActivated = false; + + dropdown.addEventListener("pointerenter", () => { + clearDesktopCloseTimer(dropdown); + + if (trigger?.getAttribute("aria-expanded") === "false") { + setDesktopDropdownExpanded(dropdown, true, "hover"); + } + }); + + dropdown.addEventListener("pointerleave", () => { + if ( + dropdown.dataset.openedBy === "hover" && + !dropdown.contains(document.activeElement) + ) { + const timer = window.setTimeout(() => { + desktopCloseTimers.delete(dropdown); + + if ( + !dropdown.matches(":hover") && + !dropdown.contains(document.activeElement) + ) { + setDesktopDropdownExpanded(dropdown, false); + } + }, 300); + + desktopCloseTimers.set(dropdown, timer); + } + }); + + trigger?.addEventListener("pointerdown", () => { + pointerActivated = true; + }); + + trigger?.addEventListener("click", () => { + const isExpanded = trigger.getAttribute("aria-expanded") === "true"; + + if ( + pointerActivated && + isExpanded && + dropdown.dataset.openedBy === "hover" + ) { + dropdown.dataset.openedBy = "activation"; + } else { + setDesktopDropdownExpanded(dropdown, !isExpanded); + } + + pointerActivated = false; + }); + + dropdown.addEventListener("focusout", (event) => { + if ( + !dropdown.contains(event.relatedTarget) && + !dropdown.matches(":hover") + ) { + setDesktopDropdownExpanded(dropdown, false); + } + }); + }); + + document.addEventListener("pointerdown", (event) => { + desktopDropdowns.forEach((dropdown) => { + if (!dropdown.contains(event.target)) { + setDesktopDropdownExpanded(dropdown, false); + } + }); + }); + + const setMenuOpen = (isOpen, returnFocus = false) => { + menu.classList.toggle("visible", isOpen); + document.body.classList.toggle("mobile-menu-open", isOpen); + hamburger.setAttribute("aria-expanded", String(isOpen)); + hamburger.setAttribute( + "aria-label", + isOpen ? "Close navigation menu" : "Open navigation menu", + ); + + if (returnFocus) hamburger.focus(); + }; hamburger.addEventListener("click", () => { - menu.classList.toggle("visible"); + setMenuOpen(!menu.classList.contains("visible")); }); menu.addEventListener("click", (event) => { if (event.target == menu) { - menu.classList.toggle("visible"); + setMenuOpen(false, true); } }); + + document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + + if (menu.classList.contains("visible")) { + setMenuOpen(false, true); + return; + } + + const openDropdown = Array.from(desktopDropdowns).find( + (dropdown) => + dropdown + .querySelector(":scope > button") + ?.getAttribute("aria-expanded") === "true", + ); + + if (openDropdown) { + const trigger = openDropdown.querySelector(":scope > button"); + setDesktopDropdownExpanded(openDropdown, false); + trigger?.focus(); + } + }); + + window + .matchMedia("(min-width: 1051px)") + .addEventListener("change", (event) => { + if (event.matches) setMenuOpen(false); + }); }); window.toggleShow = function (id) { - let x = document.getElementById("hbdd_" + id); - if (x.style.display === "none") { - x.style.display = "block"; - } else { - x.style.display = "none"; - } + const menu = document.getElementById("hamburger-menu"); + const selected = document.getElementById("hbdd_" + id); + const shouldOpen = selected.style.display === "none"; + + menu.querySelectorAll(".mobile-dropdown").forEach((dropdown) => { + const isSelected = dropdown === selected && shouldOpen; + dropdown.style.display = isSelected ? "block" : "none"; + const trigger = menu.querySelector(`[aria-controls="${dropdown.id}"]`); + trigger?.setAttribute("aria-expanded", String(isSelected)); + }); };