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) }}&nbsp;
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