diff --git a/.pugrc.js b/.pugrc.js
index 7183759..7581da0 100644
--- a/.pugrc.js
+++ b/.pugrc.js
@@ -5,21 +5,21 @@ module.exports = {
app_name: "Fappen",
manifest: require('./manifest.json'),
navigation: {
- Frontpage: "/pages/index.pug",
- Stregsystem: "/pages/stregsystem.pug",
- Songbook: "/pages/songbook/index.pug",
- Events: "/pages/events.pug",
- Links: "/pages/links.pug",
- Offline: "/pages/offline.pug"
+ Frontpage: ["/pages/index.pug", "🏠", "Frontpage"],
+ Stregsystem: ["/pages/stregsystem.pug", "💵", "Browse our collection of wares"],
+ Songbook: ["/pages/songbook/index.pug", "🎼", "Browser our collection of songs"],
+ Events: ["/pages/events.pug", "📅", "List upcoming events"],
+ Links: ["/pages/links.pug", "🌐", "Look at nice links"],
+ Offline: ["/pages/offline.pug", "✈", "Enter offline-mode"],
+ TenFoot: ["/pages/tenfoot/toggle.pug", "➜]", "Toggle 10-foot display"],
},
links: {
- Slack: "https://fklubben.slack.com",
- Discord: "https://discord.gg/6DBvANjs3g",
- Facebook: "https://www.facebook.com/fklub",
- Github: "https://github.com/f-klubben",
- Fiki: "https://fklub.dk",
- Stregsystem: "https://stregsystem.fklub.dk"
-
+ Slack: ["https://fklubben.slack.com"],
+ Discord: ["https://discord.gg/6DBvANjs3g"],
+ Facebook: ["https://www.facebook.com/fklub"],
+ Github: ["https://github.com/f-klubben"],
+ Fiki: ["https://fklub.dk"],
+ Stregsystem: ["https://stregsystem.fklub.dk"]
},
disable_worker: process.env.disable_worker === "true" || true,
}
diff --git a/components/base_layout.pug b/components/page_layout.pug
similarity index 73%
rename from components/base_layout.pug
rename to components/page_layout.pug
index d03056d..5adae03 100644
--- a/components/base_layout.pug
+++ b/components/page_layout.pug
@@ -17,7 +17,7 @@ html(lang="en")
ul.border-outer
each key in Object.keys(navigation)
.border-inner
- a(href=navigation[key])
+ a(href=navigation[key][0])
li= key
button.darkmode-button darkmode
main
@@ -28,4 +28,8 @@ html(lang="en")
if hide_frit_fit !== true
p.frit-fit #fritfit (for real)
+scripts
+ script.
+ // Determine mode from localStorage (or default)
+ const tenFoot = localStorage.getItem('tenFoot') === '1';
+ if (tenFoot) document.body.classList.add('ten-foot');
block scripts
diff --git a/components/service_menu.pug b/components/service_menu.pug
index fc5232a..59cde16 100644
--- a/components/service_menu.pug
+++ b/components/service_menu.pug
@@ -1,7 +1,12 @@
mixin menu(links, target)
link(rel="stylesheet" href="/styles/service-menu.scss")
- section.service-links.border-outer
+ link(rel="stylesheet" href="/styles/service-menu_10foot.scss")
+ section.service-menu.border-outer
each key in Object.keys(links)
- a(href=links[key] target=target)
+ a.service-link(href=links[key][0] target=target)
+ if links[key].length > 1
+ .service-icon=links[key][1]
.border-inner
h3=key
+ if links[key].length > 2
+ .service-description=links[key][2]
diff --git a/components/tenfoot_layout.pug b/components/tenfoot_layout.pug
new file mode 100644
index 0000000..48ed550
--- /dev/null
+++ b/components/tenfoot_layout.pug
@@ -0,0 +1,40 @@
+include core
+
+block pug_meta
+
+- const title = typeof page_title === 'string' ? `${page_title} - ${app_name}` : app_name;
+
+doctype html
+html(lang="en")
+ +head(title)
+ link(rel="stylesheet" href="/styles/tenfoot.scss")
+ body
+ header.header-with-logo
+ .logo-title-container
+ a.logo-wrapper(href="/tenfoot/")
+ img.logo(src="/media/flogo_w.svg" alt="Logo")
+ .title-wrapper
+ h1= app_name
+ block title
+
+ main
+ if show_load_indicator
+ #loading-indicator.spinner
+ img(width="200" src="/media/fstjerne.svg")
+
+ .nav-hint
+ span Navigate with
+ kbd ↑
+ kbd ↓
+ kbd ←
+ kbd →
+ kbd Enter
+ block content
+
+ footer
+ if hide_frit_fit !== true
+ p.frit-fit #fritfit (for real)
+
+ +scripts
+ script(type="module" src="/scripts/tenfoot/tenfoot-navigation.ts")
+ block scripts
\ No newline at end of file
diff --git a/media/flogo_w.svg b/media/flogo_w.svg
new file mode 100644
index 0000000..4d54b7d
--- /dev/null
+++ b/media/flogo_w.svg
@@ -0,0 +1,77 @@
+
+
\ No newline at end of file
diff --git a/pages/events.pug b/pages/events.pug
index a20f80d..7484deb 100644
--- a/pages/events.pug
+++ b/pages/events.pug
@@ -1,4 +1,4 @@
-extends ../components/base_layout
+extends ../components/page_layout
block pug_meta
- const page_title = "Events";
diff --git a/pages/index.pug b/pages/index.pug
index b3000b3..82781b6 100644
--- a/pages/index.pug
+++ b/pages/index.pug
@@ -1,4 +1,4 @@
-extends ../components/base_layout
+extends ../components/page_layout
include ../components/service_menu
block content
diff --git a/pages/links.pug b/pages/links.pug
index 60cba4b..1b9f0af 100644
--- a/pages/links.pug
+++ b/pages/links.pug
@@ -1,4 +1,4 @@
-extends ../components/base_layout
+extends ../components/page_layout
include ../components/service_menu
block content
diff --git a/pages/offline.pug b/pages/offline.pug
index 940a200..8a27199 100644
--- a/pages/offline.pug
+++ b/pages/offline.pug
@@ -1,4 +1,4 @@
-extends ../components/base_layout
+extends ../components/page_layout
block head
link(rel="stylesheet", href="/styles/offline.scss")
diff --git a/pages/songbook/index.pug b/pages/songbook/index.pug
index 5dd0143..d0fd213 100644
--- a/pages/songbook/index.pug
+++ b/pages/songbook/index.pug
@@ -1,4 +1,4 @@
-extends ../../components/base_layout.pug
+extends ../../components/page_layout
block head
link(rel="stylesheet" href="/styles/songbook.scss")
script(type="module", src="/scripts/songbook.ts")
diff --git a/pages/stregsystem.pug b/pages/stregsystem.pug
index 35a6c3c..941c9c8 100644
--- a/pages/stregsystem.pug
+++ b/pages/stregsystem.pug
@@ -1,4 +1,4 @@
-extends ../components/base_layout
+extends ../components/page_layout
block pug_meta
- const page_title = "Stregsystemet";
diff --git a/pages/tenfoot/index.pug b/pages/tenfoot/index.pug
new file mode 100644
index 0000000..3615c74
--- /dev/null
+++ b/pages/tenfoot/index.pug
@@ -0,0 +1,9 @@
+extends ../../components/tenfoot_layout
+
+block title
+ h2 Welcome
+ .subtitle Select a service to continue
+
+block content
+ link(rel="stylesheet" href="/styles/menu10foot.scss")
+ section.menu10foot
diff --git a/pages/tenfoot/toggle.pug b/pages/tenfoot/toggle.pug
new file mode 100644
index 0000000..f053a2b
--- /dev/null
+++ b/pages/tenfoot/toggle.pug
@@ -0,0 +1,13 @@
+include ../../components/core
+
+doctype html
+html(lang="en")
+ body
+ script.
+ // Read current mode from localStorage
+ const tenFoot = localStorage.getItem('tenFoot') === '1';
+
+ localStorage.setItem('tenFoot', tenFoot ? '0' : '1');
+
+ // Redirect after flipping
+ window.location.href = '/';
diff --git a/scripts/tenfoot/menu.ts b/scripts/tenfoot/menu.ts
new file mode 100644
index 0000000..22ac6be
--- /dev/null
+++ b/scripts/tenfoot/menu.ts
@@ -0,0 +1,84 @@
+// Keyboard navigation for 10-foot interface
+interface ServiceSelected {
+ service: string;
+}
+
+export interface GotoPage {
+ navigateTo(page: string): Promise;
+}
+
+export class TenFootMenu {
+ private cards: NodeListOf;
+ private nav: GotoPage;
+ private currentIndex: number;
+
+ constructor(navigator: GotoPage, cardContainer: Element) {
+ this.nav = navigator;
+ this.cards = cardContainer.querySelectorAll('.menu10foot-card');
+ this.currentIndex = 0;
+ this.init();
+ }
+
+ private init(): void {
+ console.log("Initialized Tenfoot Menu");
+ this.setupKeyboardNavigation();
+ this.setupClickHandlers();
+
+ // Focus first card on load
+ if (this.cards.length > 0) {
+ this.focusCard(0);
+ }
+ }
+
+ private focusCard(index: number): void {
+ if (index >= 0 && index < this.cards.length) {
+ this.cards[index].focus();
+ this.currentIndex = index;
+ }
+ }
+
+ private setupKeyboardNavigation(): void {
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
+ switch(e.key) {
+ case 'ArrowRight':
+ e.preventDefault();
+ this.focusCard((this.currentIndex + 1) % this.cards.length);
+ break;
+
+ case 'ArrowLeft':
+ e.preventDefault();
+ this.focusCard((this.currentIndex - 1 + this.cards.length) % this.cards.length);
+ break;
+
+ case 'Enter':
+ e.preventDefault();
+ this.cards[this.currentIndex].click();
+ break;
+ }
+ });
+ }
+
+ private setupClickHandlers(): void {
+ this.cards.forEach((card: HTMLElement, index: number) => {
+ card.addEventListener('click', async () => {
+ const link = card.dataset.link || '';
+ const titleElement = card.querySelector('.service-title');
+ const title = titleElement?.textContent || '';
+
+ console.log(`Selected: ${title} (${link})`);
+
+ await this.nav.navigateTo(link);
+ });
+
+ card.addEventListener('focus', () => {
+ this.currentIndex = index;
+
+ card.scrollIntoView({
+ behavior: 'smooth',
+ inline: 'center',
+ block: 'nearest'
+ });
+ });
+ });
+ }
+}
diff --git a/scripts/tenfoot/tenfoot-navigation.ts b/scripts/tenfoot/tenfoot-navigation.ts
new file mode 100644
index 0000000..189110d
--- /dev/null
+++ b/scripts/tenfoot/tenfoot-navigation.ts
@@ -0,0 +1,122 @@
+// Keyboard navigation for 10-foot interface
+
+import {TenFootMenu, GotoPage} from "./menu";
+
+class TenFootNavigator implements GotoPage {
+ private currentPage: string;
+
+ constructor() {
+ this.init();
+ }
+
+ private init(): void {
+ console.log("Loaded Tenfoot-navigation");
+
+ this.inputOverride();
+ this.handleLoading();
+ this.TryInstantiateMenu();
+ }
+
+ private inputOverride(): void {
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
+ switch(e.key) {
+ case 'Enter':
+ break;
+ case 'Escape':
+ case 'BrowserBack':
+ e.preventDefault();
+ break;
+ case 'MediaPlayPause':
+ break;
+ case 'MediaPlay':
+ break;
+ case 'MediaPause':
+ break;
+ case 'MediaStop':
+ break;
+ case 'MediaTrackNext':
+ break;
+ case 'MediaTrackPrevious':
+ break;
+ }
+ });
+ }
+
+ private handleLoading(): void {
+ const loadingIndicator = document.getElementById('loading-indicator');
+
+ if (loadingIndicator) {
+ window.addEventListener('load', () => {
+ setTimeout(() => {
+ loadingIndicator.style.display = 'none';
+ }, 1500);
+ });
+ }
+ }
+
+ async navigateTo(page: string): Promise {
+ await this.LoadPage(page);
+ }
+
+ async LoadPage(tenfoot_page: string) {
+ // Special case of navigation
+ if (tenfoot_page == "//"){
+ window.location.href = "/";
+ }else if (tenfoot_page == "/"){
+ tenfoot_page = "index";
+ }
+
+ const app = document.getElementsByTagName('section')[0]
+
+ try {
+ await fetch(`/${tenfoot_page}.html`)
+ .then(response => response.text())
+ .then(html => {
+ // Parse the HTML string into a document
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+
+ // Extract the element
+ const mainContent = doc.querySelector('main');
+ console.log(mainContent);
+
+ if (mainContent) {
+ // Inject only the inner content of
+ app.innerHTML = mainContent.innerHTML;
+ }
+ }
+ );
+
+ this.TryInstantiateMenu();
+ } catch (error) {
+ app.innerHTML = 'Page not found
';
+ }
+ }
+
+ TryInstantiateMenu(){
+ // Try load 10foot-menu handler
+ const initMenu = () => {
+ const menuContainer = document.getElementsByClassName('menu10foot')[0];
+ let nav = new TenFootMenu(this, menuContainer);
+ };
+
+ // TODO: mby doesn't work for loading subpages
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initMenu);
+ } else {
+ initMenu();
+ }
+ }
+}
+
+const initNavigation = () => {
+ let nav = new TenFootNavigator();
+};
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initNavigation);
+} else {
+ initNavigation();
+}
+
+export default TenFootNavigator;
\ No newline at end of file
diff --git a/styles/base_10foot.scss b/styles/base_10foot.scss
new file mode 100644
index 0000000..2797db9
--- /dev/null
+++ b/styles/base_10foot.scss
@@ -0,0 +1,67 @@
+body.ten-foot {
+ // Variables
+ $primary-gradient-start: #1a1a2e;
+ $primary-gradient-end: #16213e;
+ $text-color: #ffffff;
+ $focus-color: #00d4ff;
+
+ body {
+ background: linear-gradient(135deg, $primary-gradient-start 0%, $primary-gradient-end 100%);
+ color: $text-color;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ overflow-x: hidden;
+ max-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ main {
+ flex: 1;
+ padding-left: 100vw; // scuffed
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ //padding: 4rem;
+ }
+
+ .center {
+ text-align: center;
+ }
+
+ .flex-center {
+ align-content: center;
+ justify-content: center;
+ }
+
+ /*
+ Header & navigation
+ */
+
+ header {
+ display: none;
+ }
+
+ .spinner {
+ animation: rotation 5s infinite linear;
+ }
+
+ @keyframes rotation {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(359deg);
+ }
+ }
+
+ /* Border style simulating html tables */
+ .border-outer {
+ border: inherit !important;
+ }
+
+ .border-inner {
+ margin: inherit !important;
+ border: inherit !important;
+ }
+}
+
diff --git a/styles/menu10foot.scss b/styles/menu10foot.scss
new file mode 100644
index 0000000..e07a824
--- /dev/null
+++ b/styles/menu10foot.scss
@@ -0,0 +1,92 @@
+// ten-foot.scss
+// Variables
+$primary-gradient-start: #1a1a2e;
+$primary-gradient-end: #16213e;
+$text-color: #ffffff;
+$card-bg: rgba(255, 255, 255, 0.1);
+$card-bg-hover: rgba(255, 255, 255, 0.15);
+$card-border: rgba(255, 255, 255, 0.2);
+$card-border-hover: rgba(255, 255, 255, 0.4);
+$focus-color: #00d4ff;
+
+// Mixins
+@mixin card-shadow {
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+}
+
+@mixin text-shadow {
+ text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+@mixin smooth-transition($properties: all, $duration: 0.3s) {
+ transition: $properties $duration ease;
+}
+
+// Service Grid
+.menu10foot {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 3rem;
+
+ overflow-x: auto;
+ overflow-y: hidden;
+
+ scroll-behavior: smooth;
+ scroll-snap-type: x mandatory;
+ padding-inline: 3rem;
+
+ /* IE / Edge Legacy */
+ -ms-overflow-style: none;
+}
+
+.menu10foot::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Edge */
+}
+
+.menu10foot-card {
+ flex: 0 0 auto; // prevents shrinking
+ margin: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2em;
+
+ text-align: center;
+ background: $card-bg;
+ backdrop-filter: blur(10px);
+ border-radius: 2rem;
+ padding: 3em;
+ @include smooth-transition;
+ border: 2px solid $card-border;
+ cursor: pointer;
+ font-size: clamp(0.1rem, 1vw, 6rem);
+ height: 25em;
+ width: 25em;
+ scroll-margin-inline: 30rem;
+
+ &:hover,
+ &:focus {
+ transform: scale(1.05);
+ background: $card-bg-hover;
+ border-color: $card-border-hover;
+ @include card-shadow;
+ }
+
+ .menu10foot-icon {
+ font-size: 6em;
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ }
+
+ .menu10foot-title {
+ font-size: 3em;
+ font-weight: 600;
+ }
+
+ .menu10foot-description {
+ font-size: 1.8em;
+ opacity: 0.9;
+ line-height: 1.6;
+ }
+}
diff --git a/styles/service-menu.scss b/styles/service-menu.scss
index 33e4124..15e1f26 100644
--- a/styles/service-menu.scss
+++ b/styles/service-menu.scss
@@ -1,4 +1,4 @@
-.service-links {
+.service-menu {
width: 50%;
box-sizing: border-box;
text-align: center;
@@ -7,4 +7,8 @@
.service-body {
padding: 1em;
+}
+
+.service-icon, .service-description {
+ display: none;
}
\ No newline at end of file
diff --git a/styles/service-menu_10foot.scss b/styles/service-menu_10foot.scss
new file mode 100644
index 0000000..f9fb24c
--- /dev/null
+++ b/styles/service-menu_10foot.scss
@@ -0,0 +1,105 @@
+// ten-foot.scss
+// Variables
+$primary-gradient-start: #1a1a2e;
+$primary-gradient-end: #16213e;
+$text-color: #ffffff;
+$card-bg: rgba(255, 255, 255, 0.1);
+$card-bg-hover: rgba(255, 255, 255, 0.15);
+$card-border: rgba(255, 255, 255, 0.2);
+$card-border-hover: rgba(255, 255, 255, 0.4);
+$focus-color: #00d4ff;
+
+// Mixins
+@mixin card-shadow {
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+}
+
+@mixin text-shadow {
+ text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+@mixin smooth-transition($properties: all, $duration: 0.3s) {
+ transition: $properties $duration ease;
+}
+
+// Only apply rules on 10-foot mode
+body.ten-foot {
+ // Service Grid
+ .service-menu {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 3rem;
+
+ overflow-x: auto;
+ overflow-y: hidden;
+
+ scroll-behavior: smooth;
+ scroll-snap-type: x mandatory;
+ padding-inline: 3rem;
+
+ /* IE / Edge Legacy */
+ -ms-overflow-style: none;
+ }
+
+ .service-menu::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Edge */
+ }
+
+ .service-menu > a {
+ // Un-linkify
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .service-menu > .service-link {
+ flex: 0 0 auto; // prevents shrinking
+ margin: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2em;
+
+ text-align: center;
+ background: $card-bg;
+ backdrop-filter: blur(10px);
+ border-radius: 2rem;
+ padding: 3em;
+ @include smooth-transition;
+ border: 2px solid $card-border;
+ cursor: pointer;
+ font-size: clamp(0.1rem, 1vw, 6rem);
+ height: 25em;
+ width: 25em;
+ scroll-margin-inline: 30rem;
+
+ &:hover,
+ &:focus {
+ transform: scale(1.05);
+ background: $card-bg-hover;
+ border-color: $card-border-hover;
+ @include card-shadow;
+ }
+
+ div > h3 {
+ h3 {
+ font-size: 3em;
+ font-weight: 600;
+ }
+ }
+
+ .service-icon {
+ font-size: 6em;
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
+ }
+
+ .service-description {
+ font-size: 1.8em;
+ opacity: 0.9;
+ line-height: 1.6;
+ }
+
+ }
+}
diff --git a/styles/tenfoot.scss b/styles/tenfoot.scss
new file mode 100644
index 0000000..16e1667
--- /dev/null
+++ b/styles/tenfoot.scss
@@ -0,0 +1,174 @@
+// ten-foot.scss
+// Variables
+$primary-gradient-start: #1a1a2e;
+$primary-gradient-end: #16213e;
+$text-color: #ffffff;
+$focus-color: #00d4ff;
+
+// Mixins
+@mixin card-shadow {
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
+}
+
+@mixin text-shadow {
+ text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+}
+
+@mixin smooth-transition($properties: all, $duration: 0.3s) {
+ transition: $properties $duration ease;
+}
+
+// Reset
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+// Body
+html {
+ background: linear-gradient(135deg, $primary-gradient-start 0%, $primary-gradient-end 100%);
+ color: $text-color;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ overflow-x: hidden;
+ max-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+main {
+ flex: 1;
+ padding-left: 100vw; // scuffed
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ //padding: 4rem;
+}
+
+// Header with logo
+.header-with-logo {
+ padding: 1.5em;
+
+ .logo-title-container {
+ display: flex;
+ align-items: center;
+ gap: 2em;
+ }
+
+ .logo-wrapper {
+ flex-shrink: 0;
+
+ .logo {
+ width: 8em;
+ height: 8em;
+ filter: drop-shadow(0 4px 12px rgba(255, 255, 255, 0.3));
+ @include smooth-transition(transform);
+
+ &:hover {
+ transform: scale(1.05);
+ }
+ }
+ }
+
+ .title-wrapper {
+ flex: 1;
+
+ h1 {
+ font-size: 3rem;
+ font-weight: 700;
+ @include text-shadow;
+ line-height: 1.2;
+ }
+
+ .subtitle {
+ font-size: 2rem;
+ opacity: 0.8;
+ font-weight: 300;
+ }
+ }
+}
+
+// Header
+header {
+ height: auto !important;
+ font-size: clamp(0.1rem, 2vh, 6rem);
+
+ h1 {
+ font-size: 5em;
+ font-weight: 700;
+ margin-bottom: 0.2em;
+ @include text-shadow;
+ }
+
+ .subtitle {
+ font-size: 2.5em;
+ opacity: 0.8;
+ font-weight: 300;
+ }
+}
+
+// Footer
+footer {
+ padding: 2rem;
+ margin-top: auto;
+}
+.frit-fit {
+ text-align: left;
+ font-size: 1.5em;
+ opacity: 0.6;
+}
+
+// Focus indicators for accessibility
+*:focus {
+ outline: 4px solid $focus-color;
+ outline-offset: 4px;
+}
+
+// Navigation hint
+.nav-hint {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ font-size: 1.5rem;
+ opacity: 0.5;
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+
+ kbd {
+ background: rgba(255, 255, 255, 0.2);
+ padding: 0.5rem 1rem;
+ border-radius: 0.5rem;
+ font-family: monospace;
+ font-size: 1.3rem;
+ }
+}
+
+
+// Loading Indicator
+#loading-indicator {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+
+ &.spinner {
+ animation: spin 2s linear infinite;
+ }
+
+ img {
+ width: 200px;
+ height: 200px;
+ filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.5));
+ }
+}
+
+@keyframes spin {
+ 0% {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ 100% {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/util/template/song.pug b/util/template/song.pug
index 05c2a6c..ce89c4b 100644
--- a/util/template/song.pug
+++ b/util/template/song.pug
@@ -1,4 +1,4 @@
-extends ../../../components/base_layout
+extends ../../../components/page_layout
block head
link(rel="stylesheet" href="/styles/songbook.scss")
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css")