feat(design): add phase 2 site chrome (#185)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Juan Manuel Servera committed
May 25, 2026 at 21:32 UTC
95ec9fda311568f5fa60c77f15ebd8f03d51608a
15 files changed
+539
-253
.squad/agents/amy/history.md
+2
@@ -12,3 +12,5 @@
12
- 2026-05-25 Phase 1 redesign foundation: tokens live in `assets/css/tokens.css`, and Hugo loads them before PaperMod-compatible CSS from `layouts/partials/head.html`.
13
- 2026-05-25 Project-level `assets/css/core/` and `assets/css/common/` files override PaperMod submodule styles; keep the submodule clean and put SquadScope presentation changes in root assets.
14
- 2026-05-25 Inter and JetBrains Mono are loaded with preload + stylesheet links; existing PaperMod variables are mapped to new `--color-*`, `--text-*`, and `--space-*` tokens for compatibility.
15
+- 2026-05-25 Phase 2 chrome: `layouts/partials/header.html` now owns the tokenized brand/nav/GitHub/theme-toggle header, and `layouts/partials/footer.html` overrides PaperMod's footer while preserving its scroll/theme/copy-code scripts.
16
+- 2026-05-25 PaperMod gotcha: adding a skip-to-content link requires overriding `layouts/_default/baseof.html`; keep the theme submodule untouched and place site-level chrome overrides in root `layouts/partials/`.
.squad/decisions/inbox/amy-phase2-implementation.md
new
+16
@@ -0,0 +1,16 @@
1
+# Amy Phase 2 Implementation Notes
2
+
3
+Date: 2026-05-25
4
+Author: Amy
5
+Status: Implemented in PR branch
6
+
7
+## Decisions
8
+
9
+- Override PaperMod chrome at the project layer (`layouts/partials/header.html`, `layouts/partials/footer.html`) rather than editing the theme submodule.
10
+- Add `layouts/_default/baseof.html` solely to place the skip-to-content link before the cached header and give the main landmark `id="main-content"`.
11
+- Keep the primary nav intentionally scoped to Weekly, Monthly, Yearly, and About for Phase 2; archive/search/taxonomy links remain in the page body and footer where already present.
12
+- Use a native `<details>` disclosure for mobile navigation so the collapsed menu remains keyboard reachable without adding new JavaScript.
13
+
14
+## Implications
15
+
16
+Future chrome work should continue to extend root layouts and tokenized CSS. If PaperMod changes its base template, compare against this override before upgrading the theme.
assets/css/common/footer.css
+53
-30
@@ -1,56 +1,79 @@
1
-.footer,
1
+.site-footer,
2
.top-link {
3
font-size: var(--text-xs);
4
color: var(--color-text-muted);
5
}
6
7
-.footer {
7
+.site-footer {
8
+ margin-block-start: var(--space-6);
9
+ border-top: var(--size-border-thin) solid var(--color-border);
10
+ background: var(--color-surface);
11
+}
12
+
13
+.site-footer__inner {
14
+ display: grid;
15
+ gap: var(--space-3);
16
max-width: calc(var(--main-width) + var(--gap) * 2);
17
margin: auto;
10
- padding: calc((var(--footer-height) - var(--gap)) / 2) var(--gap);
18
+ padding: var(--space-6) var(--gap);
19
text-align: center;
12
- line-height: 24px;
20
}
21
15
-.footer span {
16
- margin-inline-start: 1px;
17
- margin-inline-end: 1px;
22
+.site-footer__tagline,
23
+.site-footer__meta {
24
+ margin: 0;
25
+}
26
+
27
+.site-footer__tagline {
28
+ color: var(--color-text);
29
+ font-size: var(--text-sm);
30
}
31
20
-.footer span:last-child {
21
- white-space: nowrap;
32
+.site-footer__nav {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ justify-content: center;
36
+ gap: var(--space-3);
37
}
38
24
-.footer a {
25
- color: inherit;
26
- text-underline-offset: 0.25rem;
39
+.site-footer a {
40
+ color: var(--color-text-muted);
41
+ text-underline-offset: var(--space-1);
42
text-decoration: underline;
43
}
44
45
+.site-footer a:hover,
46
+.site-footer a:focus-visible,
47
+.top-link:hover,
48
+.top-link:focus-visible {
49
+ color: var(--color-accent);
50
+}
51
+
52
.top-link {
53
position: fixed;
32
- bottom: 4rem;
33
- right: 2rem;
34
- z-index: 99;
35
- background: var(--tertiary);
36
- width: 2.5rem;
37
- height: 2.5rem;
38
- padding: 10px;
39
- border-radius: 64px;
54
+ inset-block-end: var(--space-6);
55
+ inset-inline-end: var(--space-5);
56
+ z-index: var(--z-overlay);
57
+ display: inline-flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ width: calc(var(--space-5) + var(--space-2));
61
+ height: calc(var(--space-5) + var(--space-2));
62
+ padding: var(--space-2);
63
+ border: var(--size-border-thin) solid var(--color-border);
64
+ border-radius: var(--radius-full);
65
+ background: var(--color-surface);
66
+ color: var(--color-text-muted);
67
+ box-shadow: var(--shadow-md);
68
transition: visibility .3s, opacity .3s cubic-bezier(0.4, 0, 1, 1);
69
}
70
43
-.hidden {
44
- visibility: hidden;
45
- opacity: 0;
46
-}
47
-
48
-.top-link,
71
.top-link svg {
50
- filter: drop-shadow(0px 0px 0px var(--theme));
72
+ width: var(--size-icon-md);
73
+ height: var(--size-icon-md);
74
}
75
53
-.footer a:hover,
54
-.top-link:hover {
55
- color: var(--primary);
76
+.hidden {
77
+ visibility: hidden;
78
+ opacity: 0;
79
}
assets/css/common/header.css
+197
-57
@@ -1,101 +1,241 @@
1
+.site-header {
2
+ position: sticky;
3
+ inset-block-start: 0;
4
+ z-index: var(--z-header);
5
+ border-bottom: var(--size-border-thin) solid var(--color-border);
6
+ background: var(--color-surface);
7
+}
8
+
9
.header-nav {
10
display: flex;
3
- flex-wrap: wrap;
11
+ align-items: center;
12
justify-content: space-between;
13
+ min-height: var(--size-header-height);
14
max-width: calc(var(--nav-width) + var(--gap) * 2);
15
margin: auto;
7
- line-height: var(--header-height);
16
padding: 0 var(--gap);
9
- column-gap: var(--gap);
10
-}
11
-
12
-.header-nav a {
13
- display: block;
17
+ gap: var(--space-3);
18
}
19
16
-.logo,
17
-.menu {
18
- display: flex;
19
-}
20
-
21
-.logo {
20
+.site-brand,
21
+.site-header__cluster,
22
+.site-actions,
23
+.icon-button,
24
+.site-menu__toggle {
25
+ display: inline-flex;
26
align-items: center;
23
- column-gap: 0.55rem;
24
- flex-wrap: wrap;
27
}
28
27
-.logo a {
29
+.site-brand {
30
+ gap: var(--space-2);
31
+ color: var(--color-text);
32
+ font-family: var(--font-display);
33
font-size: var(--text-xl);
34
font-weight: 700;
30
- display: flex;
31
- align-items: center;
32
- column-gap: 0.55rem;
35
+ letter-spacing: var(--tracking-tight);
36
+ white-space: nowrap;
37
}
38
35
-.logo a img,
36
-.logo a svg {
37
- pointer-events: none;
38
- border-radius: 6px;
39
+.site-brand__mark {
40
+ justify-content: center;
41
+ width: var(--size-logo-mark);
42
+ height: var(--size-logo-mark);
43
+ border: var(--size-border-thin) solid var(--color-border);
44
+ border-radius: var(--radius-md);
45
+ background: var(--color-accent-muted);
46
+ color: var(--color-accent);
47
+ font-size: var(--text-xs);
48
+ line-height: var(--size-logo-mark);
49
+ text-align: center;
50
+ letter-spacing: 0;
51
}
52
41
-.theme-toggle {
42
- padding: 0 0.4rem;
53
+.site-header__cluster {
54
+ gap: var(--space-3);
55
}
56
45
-[data-theme="dark"] .moon {
57
+.site-menu {
58
display: none;
59
+ position: relative;
60
}
61
49
-[data-theme="light"] .sun {
62
+.site-menu__toggle {
63
+ display: none;
64
+ gap: var(--space-2);
65
+ min-height: calc(var(--space-5) + var(--space-2));
66
+ padding: 0 var(--space-3);
67
+ border: var(--size-border-thin) solid var(--color-border);
68
+ border-radius: var(--radius-full);
69
+ background: var(--color-surface);
70
+ color: var(--color-text);
71
+ cursor: pointer;
72
+ font: inherit;
73
+ font-size: var(--text-sm);
74
+ font-weight: 600;
75
+}
76
+
77
+.site-menu__toggle::marker,
78
+.site-menu__toggle::-webkit-details-marker {
79
display: none;
80
}
81
53
-.logo-switches {
54
- display: inline-flex;
55
- gap: 0.4rem;
56
- align-items: inherit;
57
- min-height: stretch;
58
- flex-wrap: inherit;
82
+.site-menu__toggle-icon,
83
+.site-menu__toggle-icon::before,
84
+.site-menu__toggle-icon::after {
85
+ display: block;
86
+ width: var(--size-icon-sm);
87
+ height: var(--size-focus-ring);
88
+ border-radius: var(--radius-full);
89
+ background: currentColor;
90
}
91
61
-.logo-switches>* {
62
- min-height: inherit;
63
- align-items: center;
64
- display: inline-flex;
92
+.site-menu__toggle-icon {
93
+ position: relative;
94
}
95
67
-.nav-sep {
68
- color: var(--secondary);
96
+.site-menu__toggle-icon::before,
97
+.site-menu__toggle-icon::after {
98
+ content: "";
99
+ position: absolute;
100
+ inset-inline-start: 0;
101
}
102
71
-.lang-menu * {
72
- display: inherit;
73
- min-height: inherit;
74
- align-items: inherit;
103
+.site-menu__toggle-icon::before {
104
+ inset-block-start: calc(var(--space-1) * -1.5);
105
}
106
77
-.lang-menu a {
78
- font-size: var(--text-base);
79
- font-weight: 500;
80
- padding: 0 0.4rem;
81
- display: inline-flex
107
+.site-menu__toggle-icon::after {
108
+ inset-block-start: calc(var(--space-1) * 1.5);
109
}
110
111
.menu {
112
+ display: flex;
113
+ align-items: center;
114
+ gap: var(--space-3);
115
+ margin: 0;
116
list-style: none;
86
- word-break: keep-all;
87
- overflow-x: auto;
117
white-space: nowrap;
89
- column-gap: var(--gap);
118
}
119
120
.menu a {
93
- font-size: var(--text-base);
121
+ display: inline-flex;
122
+ align-items: center;
123
+ min-height: var(--size-header-height);
124
+ border-bottom: var(--size-focus-ring) solid transparent;
125
+ color: var(--color-text-muted);
126
+ font-size: var(--text-sm);
127
+ font-weight: 600;
128
+ letter-spacing: var(--tracking-nav);
129
+}
130
+
131
+.menu a:hover,
132
+.menu a:focus-visible,
133
+.menu a[aria-current="page"] {
134
+ color: var(--color-text);
135
+}
136
+
137
+.menu a[aria-current="page"] {
138
+ border-bottom-color: var(--color-accent);
139
+}
140
+
141
+.site-actions {
142
+ gap: var(--space-2);
143
+}
144
+
145
+.icon-button {
146
+ justify-content: center;
147
+ width: calc(var(--space-5) + var(--space-2));
148
+ height: calc(var(--space-5) + var(--space-2));
149
+ border: var(--size-border-thin) solid var(--color-border);
150
+ border-radius: var(--radius-full);
151
+ background: var(--color-surface);
152
+ color: var(--color-text-muted);
153
+ cursor: pointer;
154
+}
155
+
156
+.icon-button:hover,
157
+.icon-button:focus-visible {
158
+ color: var(--color-text);
159
+ border-color: var(--color-accent);
160
+}
161
+
162
+.icon-button svg {
163
+ width: var(--size-icon-md);
164
+ height: var(--size-icon-md);
165
+}
166
+
167
+[data-theme="dark"] .moon,
168
+:root:not([data-theme]) .sun {
169
+ display: none;
170
+}
171
+
172
+[data-theme="light"] .sun {
173
+ display: none;
174
+}
175
+
176
+[data-theme="dark"] .sun {
177
+ display: block;
178
}
179
96
-.menu .active {
97
- font-weight: 500;
98
- text-decoration: underline;
99
- text-underline-offset: 0.3rem;
100
- text-decoration-thickness: 2px;
180
+@media (max-width: 47.9375rem) {
181
+ .header-nav {
182
+ align-items: center;
183
+ }
184
+
185
+ .site-brand__text {
186
+ font-size: var(--text-lg);
187
+ }
188
+
189
+ .site-header__cluster {
190
+ gap: var(--space-2);
191
+ }
192
+
193
+ .menu--desktop {
194
+ display: none;
195
+ }
196
+
197
+ .site-menu {
198
+ display: block;
199
+ order: 2;
200
+ }
201
+
202
+ .site-menu__toggle {
203
+ display: inline-flex;
204
+ }
205
+
206
+ .site-menu__toggle-text {
207
+ display: none;
208
+ }
209
+
210
+ .site-menu:not([open]) .menu {
211
+ display: none;
212
+ }
213
+
214
+ .site-menu[open] .menu {
215
+ position: absolute;
216
+ inset-block-start: calc(100% + var(--space-2));
217
+ inset-inline-end: 0;
218
+ display: grid;
219
+ min-width: calc(var(--space-16) * 1.5);
220
+ padding: var(--space-2);
221
+ border: var(--size-border-thin) solid var(--color-border);
222
+ border-radius: var(--radius-lg);
223
+ background: var(--color-surface);
224
+ box-shadow: var(--shadow-lg);
225
+ white-space: normal;
226
+ }
227
+
228
+ .menu a {
229
+ width: 100%;
230
+ min-height: auto;
231
+ padding: var(--space-2) var(--space-3);
232
+ border-bottom: 0;
233
+ border-inline-start: var(--size-focus-ring) solid transparent;
234
+ border-radius: var(--radius-md);
235
+ }
236
+
237
+ .menu a[aria-current="page"] {
238
+ border-inline-start-color: var(--color-accent);
239
+ background: var(--color-accent-muted);
240
+ }
241
}
assets/css/core/theme-vars.css
+2
-2
@@ -3,8 +3,8 @@
3
--content-gap: var(--space-4);
4
--nav-width: 1024px;
5
--main-width: 720px;
6
- --header-height: 60px;
7
- --footer-height: 60px;
6
+ --header-height: var(--size-header-height);
7
+ --footer-height: var(--size-header-height);
8
--radius: var(--radius-md);
9
--theme: var(--color-bg);
10
--entry: var(--color-surface);
assets/css/extended/squadscope.css
+29
-39
@@ -10,7 +10,7 @@
10
.home-nav-card,
11
.metric-card,
12
.report-shortcuts {
13
- border: 1px solid var(--color-border);
13
+ border: var(--size-border-thin) solid var(--color-border);
14
border-radius: var(--radius-md);
15
background: var(--color-surface);
16
}
@@ -110,7 +110,7 @@
110
111
.home-action-secondary,
112
.report-shortcuts a {
113
- border: 1px solid var(--color-border);
113
+ border: var(--size-border-thin) solid var(--color-border);
114
}
115
116
.home-report-list {
@@ -149,7 +149,7 @@
149
150
.search-intro-card,
151
.taxonomy-card {
152
- border: 1px solid var(--color-border);
152
+ border: var(--size-border-thin) solid var(--color-border);
153
border-radius: var(--radius-md);
154
background: var(--color-surface);
155
}
@@ -189,8 +189,8 @@ select:focus-visible,
189
textarea:focus-visible,
190
summary:focus-visible,
191
[tabindex]:focus-visible {
192
- outline: 3px solid var(--color-accent);
193
- outline-offset: 3px;
192
+ outline: var(--size-focus-ring) solid var(--color-accent);
193
+ outline-offset: var(--space-1);
194
border-radius: var(--radius-sm);
195
}
196
@@ -226,7 +226,7 @@ summary:focus-visible,
226
align-items: center;
227
gap: var(--space-1);
228
padding: var(--space-1) var(--space-2);
229
- border: 1px solid var(--color-border);
229
+ border: var(--size-border-thin) solid var(--color-border);
230
border-radius: var(--radius-full);
231
background: var(--color-surface);
232
}
@@ -238,7 +238,7 @@ summary:focus-visible,
238
min-width: 1.8rem;
239
min-height: 1.8rem;
240
padding: 0 var(--space-1);
241
- border: 1px solid var(--color-border);
241
+ border: var(--size-border-thin) solid var(--color-border);
242
border-radius: var(--radius-md);
243
background: var(--color-bg);
244
color: var(--color-text);
@@ -264,7 +264,7 @@ summary:focus-visible,
264
.table-scroll {
265
margin: var(--space-3) 0;
266
overflow-x: auto;
267
- border: 1px solid var(--color-border);
267
+ border: var(--size-border-thin) solid var(--color-border);
268
border-radius: var(--radius-md);
269
background: var(--color-surface);
270
-webkit-overflow-scrolling: touch;
@@ -288,46 +288,36 @@ summary:focus-visible,
288
289
.visually-hidden {
290
position: absolute;
291
- width: 1px;
292
- height: 1px;
291
+ width: var(--size-icon-sm);
292
+ height: var(--size-icon-sm);
293
padding: 0;
294
- margin: -1px;
294
+ margin: calc(var(--size-icon-sm) * -1);
295
overflow: hidden;
296
clip: rect(0, 0, 0, 0);
297
white-space: nowrap;
298
border: 0;
299
}
300
301
-@media (max-width: 768px) {
302
- .header-nav {
303
- align-items: flex-start;
304
- }
305
-
306
- .logo,
307
- .menu {
308
- width: 100%;
309
- }
310
-
311
- .logo {
312
- justify-content: space-between;
313
- gap: var(--space-2);
314
- }
315
-
316
- .menu {
317
- flex-wrap: wrap;
318
- overflow-x: visible;
319
- white-space: normal;
320
- gap: var(--space-2);
321
- }
301
+.skip-link {
302
+ position: fixed;
303
+ inset-block-start: var(--space-3);
304
+ inset-inline-start: var(--space-3);
305
+ z-index: var(--z-overlay);
306
+ transform: translateY(calc((var(--space-8) + var(--space-3)) * -1));
307
+ padding: var(--space-2) var(--space-3);
308
+ border: var(--size-border-thin) solid var(--color-border);
309
+ border-radius: var(--radius-full);
310
+ background: var(--color-surface);
311
+ color: var(--color-text);
312
+ font-weight: 700;
313
+ box-shadow: var(--shadow-md);
314
+}
315
323
- .menu a {
324
- padding: var(--space-1) var(--space-2);
325
- border: 1px solid var(--color-border);
326
- border-radius: var(--radius-full);
327
- background: var(--color-surface);
328
- line-height: 1.4;
329
- }
316
+.skip-link:focus-visible {
317
+ transform: translateY(0);
318
+}
319
320
+@media (max-width: 768px) {
321
.post-title {
322
font-size: clamp(var(--text-2xl), 8vw, var(--text-3xl));
323
}
assets/css/tokens.css
+12
@@ -53,6 +53,18 @@
53
--radius-lg: 12px;
54
--radius-full: 9999px;
55
56
+ --size-header-height: 4rem;
57
+ --size-icon-sm: 1rem;
58
+ --size-icon-md: 1.5rem;
59
+ --size-logo-mark: 1.75rem;
60
+ --size-focus-ring: 0.1875rem;
61
+ --size-border-thin: 1px;
62
+ --z-header: 50;
63
+ --z-overlay: 100;
64
+
65
+ --tracking-tight: -0.02em;
66
+ --tracking-nav: 0.01em;
67
+
68
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05);
69
--shadow-md: 0 4px 6px rgb(0 0 0 / 0.07);
70
--shadow-lg: 0 10px 15px rgb(0 0 0 / 0.1);
content/about/_index.md
new
+11
@@ -0,0 +1,11 @@
1
++++
2
+title = 'About'
3
+date = '2026-05-25T00:00:00+02:00'
4
+draft = false
5
+summary = 'SquadScope turns GitHub activity into readable trend reports.'
6
+description = 'Learn what SquadScope publishes and how to follow its GitHub trend reports.'
7
++++
8
+
9
+SquadScope turns GitHub activity into readable weekly, monthly, and yearly trend reports for busy developers and technical leaders.
10
+
11
+The project favors editorial signal over dashboard noise: what mattered this week, what is merely loud, and what is missing from the conversation.
hugo.toml
+3
-33
@@ -63,41 +63,11 @@ rssLimit = 20
63
weight = 30
64
65
[[menu.main]]
66
- identifier = 'archive'
67
- name = 'Archive'
68
- url = '/archive/'
66
+ identifier = 'about'
67
+ name = 'About'
68
+ url = '/about/'
69
weight = 40
70
71
- [[menu.main]]
72
- identifier = 'tags'
73
- name = 'Tags'
74
- url = '/tags/'
75
- weight = 50
76
-
77
- [[menu.main]]
78
- identifier = 'categories'
79
- name = 'Categories'
80
- url = '/categories/'
81
- weight = 60
82
-
83
- [[menu.main]]
84
- identifier = 'topics'
85
- name = 'Topics'
86
- url = '/topics/'
87
- weight = 65
88
-
89
- [[menu.main]]
90
- identifier = 'search'
91
- name = 'Search'
92
- url = '/search/'
93
- weight = 70
94
-
95
- [[menu.main]]
96
- identifier = 'rss'
97
- name = 'RSS'
98
- url = '/index.xml'
99
- weight = 80
100
-
71
ignoreFiles = ['data/analyzed/.*\\.md$', 'data/metrics/.*\\.md$', 'data/metrics/.*\\.jsonl$']
72
73
[module]
layouts/_default/baseof.html
new
+32
@@ -0,0 +1,32 @@
1
+{{- if lt hugo.Version "0.146.0" }}
2
+{{- errorf "=> hugo v0.146.0 or greater is required for hugo-PaperMod to build " }}
3
+{{- end -}}
4
+
5
+<!DOCTYPE html>
6
+{{- $theme := site.Params.defaultTheme | default "auto" }}
7
+{{- if eq $theme "dark" }}
8
+<html lang="{{ site.Language }}" dir="{{ .Language.LanguageDirection | default "auto" }}" data-theme="dark">
9
+{{- else if eq $theme "light" }}
10
+<html lang="{{ site.Language }}" dir="{{ .Language.LanguageDirection | default "auto" }}" data-theme="light">
11
+{{- else }}
12
+<html lang="{{ site.Language }}" dir="{{ .Language.LanguageDirection | default "auto" }}" data-theme="auto">
13
+{{- end }}
14
+
15
+<head>
16
+ {{- partial "head.html" . }}
17
+</head>
18
+
19
+{{- if (or (ne .Kind `page` ) (eq .Layout `archives`) (eq .Layout `search`)) }}
20
+<body class="list" id="top">
21
+{{- else }}
22
+<body id="top">
23
+{{- end }}
24
+ <a class="skip-link" href="#main-content">Skip to content</a>
25
+ {{ partialCached "header.html" . .Page -}}
26
+ <main id="main-content" class="main" tabindex="-1">
27
+ {{- block "main" . }}{{ end }}
28
+ </main>
29
+ {{ partialCached "footer.html" . .Layout .Kind (.Param "hideFooter") (.Param "ShowCodeCopyButtons") -}}
30
+</body>
31
+
32
+</html>
layouts/partials/footer.html
new
+137
@@ -0,0 +1,137 @@
1
+{{- if not (.Param "hideFooter") }}
2
+<footer class="footer site-footer" aria-label="Site footer">
3
+ <div class="site-footer__inner">
4
+ <p class="site-footer__tagline"><strong>SquadScope</strong> — Weekly tech signal from GitHub.</p>
5
+ <nav class="site-footer__nav" aria-label="Footer">
6
+ <a href="https://github.com/jmservera/SquadScope" target="_blank" rel="noopener">GitHub</a>
7
+ <a href="{{ "index.xml" | absLangURL }}">RSS</a>
8
+ <a href="{{ "archive/" | absLangURL }}">Archive</a>
9
+ </nav>
10
+ <p class="site-footer__meta">© 2026 jmservera · Generated 2026-05-25 · Powered by <a href="https://gohugo.io/?utm_source=squadscope" rel="noopener" target="_blank">Hugo</a></p>
11
+ </div>
12
+</footer>
13
+{{- end }}
14
+
15
+{{- if (not site.Params.disableScrollToTop) }}
16
+<a href="#top" id="top-link" class="top-link hidden" aria-label="Go to top" title="Go to Top (Alt + G)" accesskey="g">
17
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
18
+ stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up" aria-hidden="true">
19
+ <polyline points="17 11 12 6 7 11"></polyline>
20
+ <polyline points="17 18 12 13 7 18"></polyline>
21
+ </svg>
22
+</a>
23
+{{- end }}
24
+
25
+{{- partial "extend_footer.html" . }}
26
+
27
+<script>
28
+ const menu = document.getElementById('menu');
29
+ if (menu) {
30
+ const scrollPosition = localStorage.getItem("menu-scroll-position");
31
+ if (scrollPosition) {
32
+ menu.scrollLeft = parseInt(scrollPosition, 10);
33
+ }
34
+
35
+ menu.onscroll = function () {
36
+ localStorage.setItem("menu-scroll-position", menu.scrollLeft);
37
+ };
38
+ }
39
+
40
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
41
+ anchor.addEventListener("click", function (e) {
42
+ const id = this.getAttribute("href").slice(1);
43
+ const target = document.querySelector(`[id='${decodeURIComponent(id)}']`);
44
+ if (!target) return;
45
+
46
+ e.preventDefault();
47
+ if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
48
+ target.scrollIntoView({ behavior: "smooth" });
49
+ } else {
50
+ target.scrollIntoView();
51
+ }
52
+ if (id === "top") {
53
+ history.replaceState(null, null, " ");
54
+ } else {
55
+ history.pushState(null, null, `#${id}`);
56
+ }
57
+ if (id === "main-content") {
58
+ target.focus({ preventScroll: true });
59
+ }
60
+ });
61
+ });
62
+</script>
63
+
64
+{{- if (not site.Params.disableScrollToTop) }}
65
+<script>
66
+ const toplink = document.getElementById("top-link");
67
+ window.onscroll = function () {
68
+ const scrollThreshold = window.innerHeight;
69
+ if (document.body.scrollTop > scrollThreshold || document.documentElement.scrollTop > scrollThreshold) {
70
+ toplink.classList.remove("hidden");
71
+ } else {
72
+ toplink.classList.add("hidden");
73
+ }
74
+ };
75
+</script>
76
+{{- end }}
77
+
78
+{{- if (not site.Params.disableThemeToggle) }}
79
+<script>
80
+ document.getElementById("theme-toggle")?.addEventListener("click", () => {
81
+ const html = document.querySelector("html");
82
+ if (html.dataset.theme === "dark") {
83
+ html.dataset.theme = 'light';
84
+ localStorage.setItem("pref-theme", 'light');
85
+ } else {
86
+ html.dataset.theme = 'dark';
87
+ localStorage.setItem("pref-theme", 'dark');
88
+ }
89
+ });
90
+</script>
91
+{{- end }}
92
+
93
+{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search") (.Param "ShowCodeCopyButtons")) }}
94
+<script>
95
+ document.querySelectorAll('pre > code').forEach((codeblock) => {
96
+ const container = codeblock.parentNode.parentNode;
97
+ const copybutton = document.createElement('button');
98
+ copybutton.classList.add('copy-code');
99
+ copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
100
+
101
+ function copyingDone() {
102
+ copybutton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
103
+ setTimeout(() => {
104
+ copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
105
+ }, 2000);
106
+ }
107
+
108
+ copybutton.addEventListener('click', () => {
109
+ if ('clipboard' in navigator) {
110
+ navigator.clipboard.writeText(codeblock.textContent);
111
+ copyingDone();
112
+ return;
113
+ }
114
+
115
+ const range = document.createRange();
116
+ range.selectNodeContents(codeblock);
117
+ const selection = window.getSelection();
118
+ selection.removeAllRanges();
119
+ selection.addRange(range);
120
+ try {
121
+ document.execCommand('copy');
122
+ copyingDone();
123
+ } catch (e) { };
124
+ selection.removeRange(range);
125
+ });
126
+
127
+ if (container.classList.contains("highlight")) {
128
+ container.appendChild(copybutton);
129
+ } else if (container.parentNode.firstChild == container) {
130
+ } else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
131
+ codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
132
+ } else {
133
+ codeblock.parentNode.appendChild(copybutton);
134
+ }
135
+ });
136
+</script>
137
+{{- end }}
layouts/partials/header.html
+45
-92
@@ -1,50 +1,56 @@
1
-<header class="header">
1
+<header class="header site-header">
2
<nav class="header-nav" aria-label="Primary">
3
- <div class="logo">
4
- {{- $label_text := (site.Params.label.text | default site.Title) }}
5
- {{- if site.Title }}
6
- <a href="{{ "" | absLangURL }}" accesskey="h" title="{{ $label_text }} (Alt + H)" aria-label="{{ $label_text }}">
7
- {{- if site.Params.label.icon }}
8
- {{- $img := resources.Get site.Params.label.icon }}
9
- {{- if $img }}
10
- {{- $processableFormats := (slice "jpg" "jpeg" "png" "tif" "bmp" "gif") -}}
11
- {{- if hugo.IsExtended -}}
12
- {{- $processableFormats = $processableFormats | append "webp" -}}
13
- {{- end -}}
14
- {{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) }}
15
- {{- if and (in $processableFormats $img.MediaType.SubType) (eq $prod true)}}
16
- {{- if site.Params.label.iconHeight }}
17
- {{- $img = $img.Resize (printf "x%d" site.Params.label.iconHeight) }}
18
- {{ else }}
19
- {{- $img = $img.Resize "x30" }}
20
- {{- end }}
21
- {{- end }}
22
- <img src="{{ $img.Permalink }}" alt="" aria-label="logo"
23
- height="{{- site.Params.label.iconHeight | default "30" -}}">
24
- {{- else }}
25
- <img src="{{- site.Params.label.icon | absURL -}}" alt="" aria-label="logo"
26
- height="{{- site.Params.label.iconHeight | default "30" -}}">
27
- {{- end -}}
28
- {{- else if hasPrefix site.Params.label.iconSVG "<svg" }}
29
- {{ site.Params.label.iconSVG | safeHTML }}
30
- {{- end -}}
31
- {{- $label_text -}}
32
- </a>
3
+ <a class="site-brand" href="{{ "" | absLangURL }}" accesskey="h" title="SquadScope (Alt + H)" aria-label="SquadScope home">
4
+ <span class="site-brand__mark" aria-hidden="true">SS</span>
5
+ <span class="site-brand__text">SquadScope</span>
6
+ </a>
7
+
8
+ <div class="site-header__cluster">
9
+ {{- $currentPage := . }}
10
+ {{- define "primary-menu-items" }}
11
+ {{- $currentPage := . }}
12
+ {{- range site.Menus.main }}
13
+ {{- $menuURL := .URL | relLangURL }}
14
+ {{- $pageURL := $currentPage.RelPermalink }}
15
+ {{- $isHome := eq $menuURL ("" | relLangURL) }}
16
+ {{- $isCurrent := or (and $isHome $currentPage.IsHome) (and (not $isHome) (or (eq $pageURL $menuURL) (strings.HasPrefix $pageURL $menuURL))) }}
17
+ <li>
18
+ <a href="{{ .URL | absLangURL }}"{{- if $isCurrent }} aria-current="page"{{- end }}>
19
+ {{- .Name -}}
20
+ </a>
21
+ </li>
22
+ {{- end }}
23
{{- end }}
24
+ <ul id="menu" class="menu menu--desktop">
25
+ {{- template "primary-menu-items" $currentPage }}
26
+ </ul>
27
+ <details class="site-menu">
28
+ <summary class="site-menu__toggle" aria-label="Toggle primary navigation">
29
+ <span class="site-menu__toggle-icon" aria-hidden="true"></span>
30
+ <span class="site-menu__toggle-text">Menu</span>
31
+ </summary>
32
+ <ul class="menu menu--mobile">
33
+ {{- template "primary-menu-items" $currentPage }}
34
+ </ul>
35
+ </details>
36
+
37
+ <div class="site-actions" aria-label="Site actions">
38
+ <a class="icon-button github-button" href="https://github.com/jmservera/SquadScope" target="_blank" rel="noopener" aria-label="GitHub">
39
+ <svg fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
40
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"></path>
41
+ </svg>
42
+ </a>
43
35
- {{- $lsenabled := (or (not site.Params.disableThemeToggle) (and (not site.Params.disableLangToggle) (hugo.IsMultilingual))) }}
36
- {{- if $lsenabled }}
37
- <div class="logo-switches">
44
{{- if (not site.Params.disableThemeToggle) }}
39
- <button id="theme-toggle" class="theme-toggle" accesskey="t" title="(Alt + T)" aria-label="Toggle theme">
40
- <svg class="moon" xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24"
45
+ <button id="theme-toggle" class="icon-button theme-toggle" accesskey="t" title="Toggle theme (Alt + T)" aria-label="Toggle theme">
46
+ <svg class="moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
47
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
42
- stroke-linejoin="round">
48
+ stroke-linejoin="round" aria-hidden="true">
49
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
50
</svg>
45
- <svg class="sun" xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24"
51
+ <svg class="sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
52
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
47
- stroke-linejoin="round">
53
+ stroke-linejoin="round" aria-hidden="true">
54
<circle cx="12" cy="12" r="5"></circle>
55
<line x1="12" y1="1" x2="12" y2="3"></line>
56
<line x1="12" y1="21" x2="12" y2="23"></line>
@@ -57,60 +63,7 @@
63
</svg>
64
</button>
65
{{- end }}
60
-
61
- {{- if (not site.Params.disableLangToggle) }}
62
- {{- $lang := .Lang}}
63
- {{- with site.Home.Translations }}
64
- {{- $separator := or $label_text (not site.Params.disableThemeToggle)}}
65
- {{- if $separator }}<span class="nav-sep">|</span>{{ end }}
66
- <ul class="lang-menu">
67
- {{- range . -}}
68
- {{- if ne $lang .Lang }}
69
- <li>
70
- <a href="{{- .Permalink -}}" title="{{ .Language.Params.languageAltTitle | default (.Language.LanguageName | emojify) | default (.Lang | title) }}"
71
- aria-label="{{ .Language.LanguageName | default (.Lang | title) }}">
72
- {{- if (and site.Params.displayFullLangName (.Language.LanguageName)) }}
73
- {{- .Language.LanguageName | emojify -}}
74
- {{- else }}
75
- {{- .Lang | title -}}
76
- {{- end -}}
77
- </a>
78
- </li>
79
- {{- end -}}
80
- {{- end}}
81
- </ul>
82
- {{- end }}
83
- {{- end }}
66
</div>
85
- {{- end }}
67
</div>
87
- {{- $currentPage := . }}
88
- <ul id="menu" class="menu" aria-label="Primary menu">
89
- {{- range site.Menus.main }}
90
- {{- $menu_item_url := (cond (strings.HasSuffix .URL "/") .URL (printf "%s/" .URL) ) | absLangURL }}
91
- {{- $page_url:= $currentPage.Permalink | absLangURL }}
92
- {{- $is_search := eq (site.GetPage .KeyName).Layout `search` }}
93
- {{- $is_current := eq $menu_item_url $page_url }}
94
- <li>
95
- <a href="{{ .URL | absLangURL }}" title="{{ .Title | default .Name }} {{- cond $is_search (" (Alt + /)" | safeHTMLAttr) ("" | safeHTMLAttr ) }}"
96
- {{- cond $is_search (" accesskey=/" | safeHTMLAttr) ("" | safeHTMLAttr ) }}
97
- {{- if $is_current }} aria-current="page"{{- end }}>
98
- <span {{- if $is_current }} class="active" {{- end }}>
99
- {{- .Pre }}
100
- {{- .Name -}}
101
- {{ .Post -}}
102
- </span>
103
- {{- if (findRE "://" .URL) }}
104
- <svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
105
- stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
106
- <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
107
- <path d="M15 3h6v6"></path>
108
- <path d="M10 14L21 3"></path>
109
- </svg>
110
- {{- end }}
111
- </a>
112
- </li>
113
- {{- end }}
114
- </ul>
68
</nav>
69
</header>
screenshots/design-verification/2026-05-25/home-desktop-light.png
Binary files a/screenshots/design-verification/2026-05-25/home-desktop-light.png and b/screenshots/design-verification/2026-05-25/home-desktop-light.png differ
screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png
Binary files a/screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png and b/screenshots/design-verification/2026-05-25/weekly-w22-desktop-dark.png differ
screenshots/design-verification/2026-05-25/yearly-2026-mobile-dark.png
Binary files a/screenshots/design-verification/2026-05-25/yearly-2026-mobile-dark.png and b/screenshots/design-verification/2026-05-25/yearly-2026-mobile-dark.png differ