fix(design): restore journalistic nav shell (#213)

Restore the publication-front shell that PR #205 over-pruned: dense navigation, topic shortcuts, Pagefind search, and a useful all-weeks archive while keeping the explainer on About.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed May 25, 2026 at 23:40 UTC 357bfde70aa8f064abf3c87e00149dcd3eca653b
10 files changed +449 -25
.squad/decisions/inbox/calculon-journalistic-shell-baseline.md new
+19
@@ -0,0 +1,19 @@
1 +# Decision: Journalistic shell baseline
2 +
3 +**Date:** 2026-05-25T23:31:03+02:00
4 +**Owner:** Calculon
5 +**Status:** Proposed
6 +
7 +## Decision
8 +
9 +The journalistic shell is a non-negotiable baseline for SquadScope. Navigation density, search, weekly archive access, and topic shortcuts must remain present in future home-page cleanups.
10 +
11 +## Rationale
12 +
13 +jmservera rejected the PR #205 revision because it over-pruned the publication shell. Future cleanups may relocate explanatory body content, but they must not remove the publication affordances that make the site feel like an editorial front page.
14 +
15 +## Implications
16 +
17 +- Keep top-level access to all weeks, topics, and search.
18 +- Keep a home-page rail or equivalent surfacing active topics and recent issues.
19 +- Preserve `/about/` as the home for the explainer and transparency dashboard.
assets/css/common/header.css
+51 -1
@@ -176,7 +176,7 @@
176 display: block;
177 }
178
179 -@media (max-width: 47.9375rem) {
179 +@media (max-width: 56rem) {
180 .header-nav {
181 align-items: center;
182 }
@@ -238,3 +238,53 @@
238 background: var(--color-accent-muted);
239 }
240 }
241 +
242 +.topic-shortcuts {
243 + display: flex;
244 + flex-wrap: wrap;
245 + align-items: center;
246 + gap: var(--space-2);
247 + max-width: calc(var(--nav-width) + var(--gap) * 2);
248 + margin: 0 auto;
249 + padding: var(--space-2) var(--gap);
250 + border-top: var(--size-border-thin) solid var(--color-border);
251 + color: var(--color-text-muted);
252 + font-size: var(--text-sm);
253 +}
254 +
255 +.topic-shortcuts__label {
256 + letter-spacing: 0.04em;
257 + text-transform: uppercase;
258 +}
259 +
260 +.topic-shortcuts a {
261 + display: inline-flex;
262 + align-items: center;
263 + min-height: calc(var(--space-4) + var(--space-1));
264 + padding: 0 var(--space-2);
265 + border: var(--size-border-thin) solid var(--color-border);
266 + border-radius: var(--radius-full);
267 + background: var(--color-bg);
268 + color: var(--color-text);
269 + font-weight: 700;
270 + text-decoration: none;
271 +}
272 +
273 +.topic-shortcuts a:hover,
274 +.topic-shortcuts a:focus-visible {
275 + border-color: var(--color-accent);
276 + color: var(--color-accent);
277 +}
278 +
279 +.topic-shortcuts__utility {
280 + background: var(--color-accent-muted) !important;
281 + color: var(--color-accent) !important;
282 +}
283 +
284 +@media (max-width: 56rem) {
285 + .topic-shortcuts {
286 + overflow-x: auto;
287 + flex-wrap: nowrap;
288 + padding-block: var(--space-2);
289 + }
290 +}
assets/css/extended/squadscope.css
+157
@@ -765,3 +765,160 @@ summary:focus-visible,
765 text-transform: uppercase;
766 }
767
768 +
769 +.section-heading-row {
770 + display: flex;
771 + align-items: flex-end;
772 + justify-content: space-between;
773 + gap: var(--space-3);
774 +}
775 +
776 +.section-heading-row h2 {
777 + margin: var(--space-1) 0 0;
778 + font-size: var(--text-xl);
779 + line-height: var(--leading-tight);
780 +}
781 +
782 +.section-heading-row > a {
783 + color: var(--color-accent);
784 + font-size: var(--text-sm);
785 + font-weight: 700;
786 + white-space: nowrap;
787 +}
788 +
789 +.home-content-grid,
790 +.home-rail,
791 +.home-quick-links,
792 +.home-topic-rail,
793 +.home-topic-list,
794 +.weekly-archive,
795 +.weekly-archive-list {
796 + display: grid;
797 + gap: var(--space-3);
798 +}
799 +
800 +.home-tag-strip {
801 + display: flex;
802 + flex-wrap: wrap;
803 + align-items: center;
804 + gap: var(--space-2);
805 + padding: var(--space-2) var(--space-3);
806 + border-block: 1px solid var(--color-border);
807 +}
808 +
809 +.home-tag-strip a,
810 +.home-topic-card,
811 +.home-quick-links a {
812 + color: var(--color-text);
813 + font-weight: 700;
814 + text-decoration-thickness: 1px;
815 + text-underline-offset: var(--space-1);
816 +}
817 +
818 +.home-tag-strip a {
819 + padding: var(--space-1) var(--space-2);
820 + border-radius: var(--radius-full);
821 + background: var(--color-accent-muted);
822 + color: var(--color-accent);
823 + text-decoration: none;
824 +}
825 +
826 +.home-recent,
827 +.home-topic-rail,
828 +.home-quick-links,
829 +.weekly-archive-entry {
830 + border: 1px solid var(--color-border);
831 + border-radius: var(--radius-md);
832 + background: var(--color-surface);
833 +}
834 +
835 +.home-recent,
836 +.home-topic-rail,
837 +.home-quick-links {
838 + padding: var(--space-4);
839 +}
840 +
841 +.home-topic-list {
842 + gap: var(--space-2);
843 +}
844 +
845 +.home-topic-card {
846 + display: flex;
847 + justify-content: space-between;
848 + gap: var(--space-2);
849 + padding: var(--space-2) 0;
850 + border-bottom: 1px solid var(--color-border);
851 + text-decoration: none;
852 +}
853 +
854 +.home-topic-card:last-child {
855 + border-bottom: 0;
856 +}
857 +
858 +.home-topic-card small {
859 + color: var(--color-text-muted);
860 + font-size: var(--text-sm);
861 + font-weight: 600;
862 + white-space: nowrap;
863 +}
864 +
865 +.home-quick-links {
866 + align-content: start;
867 +}
868 +
869 +.home-quick-links a {
870 + display: flex;
871 + justify-content: space-between;
872 + gap: var(--space-3);
873 + padding: var(--space-2) 0;
874 + border-bottom: 1px solid var(--color-border);
875 +}
876 +
877 +.home-quick-links a::after {
878 + content: "→";
879 + color: var(--color-accent);
880 +}
881 +
882 +.home-quick-links a:last-child {
883 + border-bottom: 0;
884 +}
885 +
886 +.weekly-archive-entry {
887 + display: grid;
888 + gap: var(--space-2);
889 + padding: var(--space-4);
890 +}
891 +
892 +.weekly-archive-entry h3 {
893 + margin: 0;
894 + font-size: var(--text-xl);
895 + line-height: var(--leading-tight);
896 +}
897 +
898 +.weekly-archive-entry p {
899 + margin: 0;
900 + color: var(--color-text-muted);
901 +}
902 +
903 +.weekly-archive-entry__meta {
904 + display: flex;
905 + flex-wrap: wrap;
906 + gap: var(--space-2);
907 + color: var(--color-text-muted);
908 + font-size: var(--text-sm);
909 + letter-spacing: 0.04em;
910 + text-transform: uppercase;
911 +}
912 +
913 +@media (min-width: 769px) {
914 + .home-content-grid {
915 + grid-template-columns: minmax(0, 2fr) minmax(calc(var(--space-16) + var(--space-10)), 1fr);
916 + }
917 +}
918 +
919 +@media (max-width: 640px) {
920 + .section-heading-row {
921 + align-items: flex-start;
922 + flex-direction: column;
923 + }
924 +}
content/search/_index.md
+1 -1
@@ -2,7 +2,7 @@
2 title = 'Search'
3 date = '2026-05-18T10:27:35.339+02:00'
4 draft = false
5 -layout = 'pagefind-search'
5 +layout = 'search'
6 pagefind = true
7 summary = 'Search weekly, monthly, and yearly SquadScope reports with Pagefind filters.'
8 +++
content/weekly/_index.md
+1
@@ -3,6 +3,7 @@ title = 'Weekly summaries'
3 date = '2026-05-18T10:27:35.339+02:00'
4 draft = false
5 summary = 'Browse each weekly SquadScope report.'
6 +aliases = ['/weeks/']
7 +++
8
9 Weekly editions collect the most notable new repositories, star surges, and critical analysis for each publishing cycle.
hugo.toml
+17 -5
@@ -49,33 +49,45 @@ rssLimit = 20
49 [menu]
50 [[menu.main]]
51 identifier = 'weekly'
52 - name = 'Weekly'
52 + name = 'All weeks'
53 url = '/weekly/'
54 weight = 10
55
56 + [[menu.main]]
57 + identifier = 'topics'
58 + name = 'Topics'
59 + url = '/topics/'
60 + weight = 20
61 +
62 [[menu.main]]
63 identifier = 'monthly'
64 name = 'Monthly'
65 url = '/monthly/'
60 - weight = 20
66 + weight = 30
67
68 [[menu.main]]
69 identifier = 'yearly'
70 name = 'Yearly'
71 url = '/yearly/'
66 - weight = 30
72 + weight = 40
73 +
74 + [[menu.main]]
75 + identifier = 'search'
76 + name = 'Search'
77 + url = '/search/'
78 + weight = 50
79
80 [[menu.main]]
81 identifier = 'methodology'
82 name = 'Methodology'
83 url = '/methodology/'
72 - weight = 40
84 + weight = 60
85
86 [[menu.main]]
87 identifier = 'about'
88 name = 'About'
89 url = '/about/'
78 - weight = 50
90 + weight = 70
91
92 ignoreFiles = ['data/analyzed/.*\\.md$', 'data/metrics/.*\\.md$', 'data/metrics/.*\\.jsonl$']
93
layouts/index.html
+66 -18
@@ -1,7 +1,7 @@
1 {{- define "main" -}}
2 {{- $weeklyPages := (where site.RegularPages "Type" "weekly").ByDate.Reverse -}}
3 {{- $latest := cond (gt (len $weeklyPages) 0) (index $weeklyPages 0) nil -}}
4 -{{- $recent := after 1 $weeklyPages -}}
4 +{{- $topTags := first 6 site.Taxonomies.tags.ByCount -}}
5
6 <div class="home-shell">
7 <section class="home-hero home-hero--minimal" aria-labelledby="home-title">
@@ -21,6 +21,7 @@
21 {{ partial "report-metrics.html" $latest }}
22 <div class="home-actions">
23 <a class="home-action-primary" href="{{ $latest.RelPermalink }}">Read full analysis</a>
24 + <a class="home-action-secondary" href="{{ `weekly/` | absLangURL }}">View all weeks</a>
25 </div>
26 </article>
27 {{- else -}}
@@ -31,24 +32,71 @@
32 </section>
33 {{- end -}}
34
34 - {{- if gt (len $recent) 0 -}}
35 - <section class="home-recent home-recent--compact" aria-labelledby="recent-reports-heading">
36 - <div class="section-topline">Recent archive</div>
37 - <h2 id="recent-reports-heading">Previous weekly analyses</h2>
38 - <div class="home-report-list home-report-list--compact">
39 - {{- range first 6 $recent -}}
40 - <article class="home-report-card home-report-card--compact">
41 - <a class="home-report-title" href="{{ .RelPermalink }}">
42 - <span>{{ .Title }}</span>
43 - </a>
44 - <div class="home-report-meta">
45 - {{- with .Params.week }}<span>{{ . }}</span>{{- end }}
46 - <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
35 + {{- if $topTags -}}
36 + <nav class="home-tag-strip" aria-label="Active topic shortcuts">
37 + <span class="home-tag-label">Active topics</span>
38 + {{- range $topTags }}
39 + <a href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">{{ .Name }}</a>
40 + {{- end }}
41 + <a href="{{ `topics/` | absLangURL }}">All topics</a>
42 + </nav>
43 + {{- end -}}
44 +
45 + <div class="home-content-grid">
46 + {{- if gt (len $weeklyPages) 0 -}}
47 + <section class="home-recent" aria-labelledby="recent-reports-heading">
48 + <div class="section-heading-row">
49 + <div>
50 + <div class="section-topline">Recent issues</div>
51 + <h2 id="recent-reports-heading">Weekly archive</h2>
52 + </div>
53 + <a href="{{ `weekly/` | absLangURL }}">View all weeks</a>
54 + </div>
55 + <div class="home-report-list">
56 + {{- range first 6 $weeklyPages -}}
57 + <article class="home-report-card">
58 + <a class="home-report-title" href="{{ .RelPermalink }}">
59 + <span class="home-report-week">{{ .Params.week | default (.Date.Format "2006-01-02") }}</span>
60 + <span>{{ .Title }}</span>
61 + </a>
62 + <p>{{ .Params.summary | default .Summary }}</p>
63 + <div class="home-report-meta">
64 + <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
65 + {{- with .Params.repos_featured }}<span>{{ . }} repos featured</span>{{- end }}
66 + </div>
67 + </article>
68 + {{- end -}}
69 + </div>
70 + </section>
71 + {{- end -}}
72 +
73 + <aside class="home-rail" aria-labelledby="home-rail-heading">
74 + {{- if $topTags -}}
75 + <section class="home-topic-rail" aria-labelledby="home-rail-heading">
76 + <div class="section-topline">Topics to follow</div>
77 + <h2 id="home-rail-heading">Active storylines</h2>
78 + <div class="home-topic-list">
79 + {{- range $topTags -}}
80 + <a class="home-topic-card" href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">
81 + <span>{{ .Name }}</span>
82 + <small>{{ .Count }} issue{{ if ne .Count 1 }}s{{ end }}</small>
83 + </a>
84 + {{- end -}}
85 </div>
48 - </article>
86 + </section>
87 {{- end -}}
50 - </div>
51 - </section>
52 - {{- end -}}
88 +
89 + <nav class="home-quick-links" aria-labelledby="quick-links-heading">
90 + <div class="section-topline">Quick links</div>
91 + <h2 id="quick-links-heading">Move through the report</h2>
92 + <a href="{{ `weekly/` | absLangURL }}">All weeks</a>
93 + <a href="{{ `topics/` | absLangURL }}">Topic index</a>
94 + <a href="{{ `monthly/` | absLangURL }}">Monthly rollups</a>
95 + <a href="{{ `yearly/` | absLangURL }}">Yearly rollups</a>
96 + <a href="{{ `search/` | absLangURL }}">Search</a>
97 + <a href="{{ `index.xml` | absLangURL }}">RSS feed</a>
98 + </nav>
99 + </aside>
100 + </div>
101 </div>
102 {{- end -}}
layouts/partials/header.html
+12
@@ -80,4 +80,16 @@
80 </div>
81 </div>
82 </nav>
83 +
84 + {{- $featuredTags := first 5 site.Taxonomies.tags.ByCount }}
85 + {{- if $featuredTags }}
86 + <nav class="topic-shortcuts" aria-label="Topic shortcuts">
87 + <span class="topic-shortcuts__label">Topics</span>
88 + {{- range $featuredTags }}
89 + <a href="{{ printf "tags/%s/" (.Name | urlize) | absLangURL }}">{{ .Name }}</a>
90 + {{- end }}
91 + <a class="topic-shortcuts__utility" href="{{ `weekly/` | absLangURL }}">All weeks</a>
92 + <a class="topic-shortcuts__utility" href="{{ `search/` | absLangURL }}">Search</a>
93 + </nav>
94 + {{- end }}
95 </header>
layouts/search/search.html new
+85
@@ -0,0 +1,85 @@
1 +{{- define "main" -}}
2 +<header class="page-header">
3 + {{- partial "breadcrumbs.html" . }}
4 + <h1>{{ .Title }}</h1>
5 + {{- with (.Description | default .Params.summary) }}
6 + <div class="post-description">{{ . }}</div>
7 + {{- end }}
8 +</header>
9 +
10 +{{- if .Content }}
11 +<div class="post-content md-content">
12 + {{ .Content }}
13 +</div>
14 +{{- end }}
15 +
16 +<section class="search-shell" aria-labelledby="search-interface-title">
17 + <div class="search-intro-card">
18 + <span class="section-topline">Static search</span>
19 + <h2 id="search-interface-title">Find trends by keyword or filter</h2>
20 + <p>Search every published report, then narrow the results by category, tag, year, or featured repository.</p>
21 + <div class="search-shortcuts" id="search-shortcuts" aria-label="Search keyboard shortcuts">
22 + <span class="search-shortcuts-title">Keyboard</span>
23 + <span class="search-shortcut"><kbd>/</kbd><span>Focus search</span></span>
24 + <span class="search-shortcut"><kbd>Tab</kbd><span>Move through filters and results</span></span>
25 + <span class="search-shortcut"><kbd>Enter</kbd><span>Open a result</span></span>
26 + </div>
27 + <p id="search-help" class="visually-hidden">Search every SquadScope report by keyword or filter. Use slash to focus the search field, Tab to move through filters and results, and Enter to open a result.</p>
28 + </div>
29 + <div id="search" class="search-panel" role="search" aria-labelledby="search-interface-title" aria-describedby="search-help search-shortcuts"></div>
30 +</section>
31 +
32 +<script>
33 + window.addEventListener("DOMContentLoaded", function () {
34 + const searchElement = document.getElementById("search");
35 + if (!searchElement || !window.PagefindUI) return;
36 +
37 + const applyA11y = function () {
38 + const input = searchElement.querySelector(".pagefind-ui__search-input");
39 + if (input) {
40 + input.setAttribute("aria-label", "Search SquadScope reports");
41 + input.setAttribute("aria-describedby", "search-help search-shortcuts");
42 + }
43 +
44 + const results = searchElement.querySelector(".pagefind-ui__results");
45 + if (results) {
46 + results.setAttribute("aria-live", "polite");
47 + results.setAttribute("aria-label", "Search results");
48 + }
49 +
50 + const clearButton = searchElement.querySelector(".pagefind-ui__search-clear");
51 + if (clearButton && !clearButton.getAttribute("aria-label")) {
52 + clearButton.setAttribute("aria-label", "Clear search query");
53 + }
54 +
55 + searchElement.querySelectorAll("button").forEach(function (button) {
56 + if (!button.getAttribute("aria-label")) {
57 + const label = button.textContent.trim();
58 + if (label) {
59 + button.setAttribute("aria-label", label);
60 + }
61 + }
62 + });
63 + };
64 +
65 + const observer = new MutationObserver(applyA11y);
66 + observer.observe(searchElement, { childList: true, subtree: true });
67 +
68 + new PagefindUI({
69 + element: "#search",
70 + bundlePath: "{{ `pagefind/` | absURL }}",
71 + showSubResults: true,
72 + showImages: false,
73 + excerptLength: 24,
74 + focusOnSlash: true,
75 + sort: { date: "desc" },
76 + openFilters: ["Categories", "Tags", "Repository", "Year"],
77 + translations: {
78 + placeholder: "Search reports, tags, categories, or repos"
79 + }
80 + });
81 +
82 + requestAnimationFrame(applyA11y);
83 + });
84 +</script>
85 +{{- end -}}
layouts/weekly/list.html new
+40
@@ -0,0 +1,40 @@
1 +{{- define "main" -}}
2 +{{- $weeklyPages := (where site.RegularPages "Type" "weekly").ByDate.Reverse -}}
3 +<header class="page-header">
4 + {{- partial "breadcrumbs.html" . }}
5 + <p class="section-topline">Weekly archive</p>
6 + <h1>{{ .Title }}</h1>
7 + {{- with (.Description | default .Params.summary) }}
8 + <div class="post-description">{{ . }}</div>
9 + {{- end }}
10 +</header>
11 +
12 +{{- if .Content }}
13 +<div class="post-content md-content">
14 + {{ .Content }}
15 +</div>
16 +{{- end }}
17 +
18 +<section class="weekly-archive" aria-labelledby="weekly-archive-heading">
19 + <div class="section-heading-row">
20 + <div>
21 + <div class="section-topline">All editions</div>
22 + <h2 id="weekly-archive-heading">{{ len $weeklyPages }} weekly issue{{ if ne (len $weeklyPages) 1 }}s{{ end }}</h2>
23 + </div>
24 + <a href="{{ `archive/` | absLangURL }}">Full site archive</a>
25 + </div>
26 +
27 + <div class="weekly-archive-list">
28 + {{- range $weeklyPages -}}
29 + <article class="weekly-archive-entry">
30 + <div class="weekly-archive-entry__meta">
31 + {{- with .Params.week }}<span>{{ . }}</span>{{- end }}
32 + <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
33 + </div>
34 + <h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
35 + <p>{{ .Params.summary | default .Summary }}</p>
36 + </article>
37 + {{- end -}}
38 + </div>
39 +</section>
40 +{{- end -}}