Cannabis Freedom Index

Changelog f6d4710tree, with Claude Opus 5.5

Country profile: neighbours by score under the score over time

The country among its land neighbours (borders.js), ranked by score, each linked, with a link to compare it with the closest of them. A long list keeps the rows around the country; an island is shown among the countries of its region with the most similar scores.

2 files of code, design or docs +40 −0 lines

Code, design and documents 2 files

src/components/CountryArticle.astro +27 −0
@@ -25,4 +25,5 @@ import { Image } from "astro:assets";
 import { cultureOf, cultureSection } from "../lib/culture.js";
 import { factsOf } from "../lib/facts.js";
+import { neighboursOf } from "../lib/borders.js";
 import FactText from "./FactText.astro";
 
@@ -119,4 +120,15 @@ const sections = [
 ].filter(Boolean);
 
+// Beside the score: the country among its land neighbours, by score. A country with none (an island)
+// is shown among those nearest to it in rank in its region. A long list keeps the rows around the
+// country itself. Not on a state's page.
+const landNeighbours = sub ? [] : (neighboursOf.get(c.id) ?? []).map((id) => countries.find((x) => x.id === id)).filter(Boolean);
+const around = (list, size) => { const at = list.indexOf(c), from = Math.max(0, Math.min(at - Math.floor(size / 2), list.length - size)); return list.slice(from, from + size); };
+const nearby = sub || c.territory ? [] : landNeighbours.length
+  ? around([c, ...landNeighbours].sort((a, b) => b.raw - a.raw || a.name.localeCompare(b.name)), 9)
+  : around(countries.filter((x) => x.region === c.region), 5);
+// the compare page takes up to six countries: this one and the nearest in score
+const compareIds = [c, ...nearby.filter((x) => x !== c).sort((a, b) => Math.abs(a.raw - c.raw) - Math.abs(b.raw - c.raw))].slice(0, 4).map((x) => x.slug);
+
 // The score's shape: the six segments as a radar, over the world average (and, for a state, its country).
 const radarPt = (i, val, r = 40) => { const a = -Math.PI / 2 + (i * 2 * Math.PI) / SEGMENTS.length; return [50 + Math.cos(a) * r * (val / 10), 50 + Math.sin(a) * r * (val / 10)]; };
@@ -309,4 +321,19 @@ const uncited = srcList.filter((x) => !citedSet.has(refKey(x)));
         </figure>
         {scored.length > 0 && <CountryChart history={scored} context={history.filter((ch) => !ch.score)} />}
+        {nearby.length > 1 && (
+          <div class="c-near">
+            <h2>{landNeighbours.length ? `Neighbours${landNeighbours.length + 1 > nearby.length ? ` · ${nearby.length} of ${landNeighbours.length + 1}` : ""}` : `Similar scores in ${c.region}`}</h2>
+            <ol>
+              {nearby.map((x) => (
+                <li class={x === c ? "on" : undefined}>
+                  {x === c ? <span>{x.flag} {x.name}</span> : <a href={`${base}country/${x.slug}/`}>{x.flag} {x.name}</a>}
+                  <i style={`--w:${x.raw}%;--c:${tierColor(x.raw)}`}></i>
+                  <b>{x.score}</b>
+                </li>
+              ))}
+            </ol>
+            <p><a href={`${base}compare/?c=${compareIds.join(",")}`}>Compare {landNeighbours.length ? "with neighbours" : "them"} →</a></p>
+          </div>
+        )}
       </div>
       <div class="score-rows">

src/styles/global.css +13 −0
@@ -1393,4 +1393,17 @@ details.segrow .segrow-top > span, .segrow:not(details) .segrow-top > span { fon
 .c-radar:has(.key-avg:hover, .key-avg:focus-visible) .c-radar-label .avg { display: inline; }
 .score-side .c-chart { margin: 0; }
+/* the country among its land neighbours, by score, under its score over time */
+.c-near h2 { margin: 0 0 10px; color: var(--muted); font: 400 10.5px var(--mono); text-transform: uppercase; letter-spacing: .08em; }
+.c-near ol { margin: 0; padding: 0; list-style: none; }
+.c-near li { display: grid; grid-template-columns: minmax(0, 1fr) 34% 28px; gap: 12px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 13.5px; color: var(--text-2); }
+.c-near li > span, .c-near li > a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; }
+.c-near li > a:hover { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }
+.c-near li i { display: block; height: 4px; width: var(--w); min-width: 2px; background: var(--c); opacity: .55; }
+.c-near li b { text-align: right; font: 400 12.5px var(--mono); }
+.c-near li.on { color: var(--text); font-weight: 600; }
+.c-near li.on i { opacity: 1; }
+.c-near p { margin: 10px 0 0; font-size: 12.5px; }
+.c-near p a { color: var(--text-2); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 3px; }
+.c-near p a:hover { color: var(--text); text-decoration-color: currentColor; }
 /* a segment read in the text lights its corner of the shape */
 .score-grid:has(.segrow[data-seg="possession"]:hover) [data-seg-dot="possession"],

    ↑↓ to move, ↵ to open.