Cannabis Freedom Index

Changelog e2f4c23

better look for rest of web

5 files of code, design or docs +295 −130 lines

Code, design and documents 5 files

src/components/CountryArticle.astro +11 −15
@@ -41,9 +41,8 @@ const travel = sub ? null : [
 ].filter(Boolean);
 const answers = answersFor(v.s);
-const r = 42;
-const circ = 2 * Math.PI * r;
 ---
 <article class="country" data-id={c.id}>
-  <div class="c-side">
+  {/* The masthead: name, verdict and the one big number; then the five plain answers. */}
+  <div class="c-top">
     <div class="d-head">
       <span class="d-flag">{c.flag}</span>
@@ -68,11 +67,7 @@ const circ = 2 * Math.PI * r;
       </div>
     )}
-    <div class="d-score">
-      <svg class="ring" width="104" height="104" viewBox="0 0 104 104" aria-hidden="true">
-        <circle cx="52" cy="52" r={r} fill="none" stroke="var(--border)" stroke-width="9" />
-        <circle cx="52" cy="52" r={r} fill="none" stroke={tierColor(v.raw)} stroke-width="9" stroke-linecap="round"
-          stroke-dasharray={`${(v.raw / 100) * circ} ${circ}`} transform="rotate(-90 52 52)" />
-        <text x="52" y="62" text-anchor="middle">{v.score}</text>
-      </svg>
+    <div class="d-score" style={`--c:${tierColor(v.raw)}`}>
+      <p class="d-num" aria-label={`Score ${v.score} out of 100`}><b>{v.score}</b><span>/100</span></p>
+      <div class="d-meter" aria-hidden="true"><i style={`width:${v.raw}%`}></i></div>
       <div>
         <div class="tier">{tierOf(v.raw).name}</div>
@@ -84,4 +79,5 @@ const circ = 2 * Math.PI * r;
       </div>
     </div>
+    <p class="d-note">{v.note}</p>
     <div class="d-chips">
       <span class={`chip s-${v.rec}`} title={REC_HELP[v.rec]}>Recreational: {REC_LABEL[v.rec]}</span>
@@ -100,5 +96,4 @@ const circ = 2 * Math.PI * r;
       </div>
     )}
-    <p class="d-note">{v.note}</p>
     {c.disputed && !sub && (
       <p class="c-disputed"><strong>Disputed territory.</strong> {c.disputed.note} See also <a href={`${base}country/${c.disputed.country.slug}/`} data-country={c.disputed.country.id}>{c.disputed.country.flag} {c.disputed.country.name}</a>.</p>
@@ -117,4 +112,8 @@ const circ = 2 * Math.PI * r;
     </ul>
     <p class="d-hint">A rough guide read from the scores below, not legal advice. The details and the exceptions are in each segment: select an answer to jump to it.</p>
+  </div>
+
+  {/* Figures at a glance: users, prices and the score over time. A sticky rail on the full page. */}
+  <div class="c-rail">
     {use && (
       <dl class="c-use">
@@ -134,6 +133,5 @@ const circ = 2 * Math.PI * r;
     )}
     {SHOW_PRICES && !sub && <PricePanel id={c.id} />}
-    {/* in the drawer the chart stays in this column; on the full page it moves under the breakdown */}
-    {scored.length > 0 && <div class="chart-in-side"><CountryChart history={scored} context={history.filter((ch) => !ch.score)} /></div>}
+    {scored.length > 0 && <CountryChart history={scored} context={history.filter((ch) => !ch.score)} />}
   </div>
 
@@ -195,6 +193,4 @@ const circ = 2 * Math.PI * r;
       );
     })}
-    {scored.length > 0 && <div class="chart-in-main"><CountryChart history={scored} context={history.filter((ch) => !ch.score)} /></div>}
-
     {group && (
       <section class="c-units">

src/components/DidYouKnow.astro +36 −37
@@ -1,31 +1,34 @@
 ---
-// A rotating "Did you know?" card. Every fact is in the HTML; the script only
-// decides which one is visible, so the first fact still shows without JavaScript.
+// "Did you know?": curious cases from the scores, the country write-ups and the history,
+// three at a time. Every case is in the HTML and its links work without JavaScript; the
+// script only decides which three are showing. Country links open the drawer on the home page.
 import { getFacts } from "../lib/facts.js";
 
 const base = import.meta.env.BASE_URL;
 const facts = getFacts();
+const PER_PAGE = 3;
 ---
-<section class="dyk card" id="dyk" aria-roledescription="carousel" aria-label="Did you know?">
+<section class="dyk" id="dyk" aria-label="Did you know?" data-per-page={PER_PAGE}>
   <div class="dyk-top">
-    <p class="dyk-label"><span aria-hidden="true">💡</span> Did you know?</p>
+    <h2>Did you know?</h2>
     <div class="dyk-nav">
-      <span class="dyk-count" id="dykCount">1 / {facts.length}</span>
-      <button class="icon-btn" id="dykPrev" aria-label="Previous fact">‹</button>
-      <button class="icon-btn" id="dykNext" aria-label="Next fact">›</button>
+      <span class="dyk-count" id="dykCount">1 / {Math.ceil(facts.length / PER_PAGE)}</span>
+      <button type="button" id="dykPrev" aria-label="Previous three">←</button>
+      <button type="button" id="dykNext" aria-label="Next three">→</button>
     </div>
   </div>
-  <div class="dyk-facts" aria-live="polite">
+  <ol class="dyk-facts" aria-live="polite">
     {facts.map((f, i) => (
-      <div class="dyk-fact" hidden={i !== 0}>
-        <p>{f.text}</p>
-        {f.country && (
-          <a href={`${base}country/${f.country.slug}/`} data-country={f.country.id}>
-            {f.country.flag} See {f.country.name} →
-          </a>
-        )}
-      </div>
+      <li class="dyk-fact" hidden={i >= PER_PAGE}>
+        <p class="dyk-kicker">{f.kicker}</p>
+        <p class="dyk-text">
+          {f.parts.map((part) => typeof part === "string" ? part
+            : part.country ? <a href={`${base}country/${part.country.slug}/`} data-country={part.country.id}>{part.label}</a>
+            : <a href={`${base}history/${part.change.id}/`}>{part.label}</a>)}
+        </p>
+        {f.history && <a class="dyk-more" href={`${base}history/${f.history.id}/`}>The full story →</a>}
+      </li>
     ))}
-  </div>
+  </ol>
 </section>
 
@@ -34,25 +37,21 @@ const facts = getFacts();
   const facts = [...root.querySelectorAll(".dyk-fact")];
   const count = document.getElementById("dykCount");
-  let i = 0;
-  let timer = null;
-
+  const per = +root.dataset.perPage;
+  // shuffle once per visit, so every page of three is a different mix
+  for (let k = facts.length - 1; k > 0; k--) {
+    const j = Math.floor(Math.random() * (k + 1));
+    [facts[k], facts[j]] = [facts[j], facts[k]];
+  }
+  const list = root.querySelector(".dyk-facts");
+  facts.forEach((el) => list.append(el));
+  const pages = Math.ceil(facts.length / per);
+  let page = 0;
   function show(n) {
-    i = (n + facts.length) % facts.length;
-    facts.forEach((el, k) => (el.hidden = k !== i));
-    count.textContent = `${i + 1} / ${facts.length}`;
+    page = (n + pages) % pages;
+    facts.forEach((el, k) => (el.hidden = Math.floor(k / per) !== page));
+    count.textContent = `${page + 1} / ${pages}`;
   }
-  // Rotate slowly, but never while someone is reading or interacting, and not at all
-  // for people who have asked for reduced motion.
-  const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
-  function start() { if (!still && !timer) timer = setInterval(() => show(i + 1), 9000); }
-  function stop() { clearInterval(timer); timer = null; }
-
-  document.getElementById("dykPrev").addEventListener("click", () => { stop(); show(i - 1); });
-  document.getElementById("dykNext").addEventListener("click", () => { stop(); show(i + 1); });
-  root.addEventListener("pointerenter", stop);
-  root.addEventListener("pointerleave", start);
-  root.addEventListener("focusin", stop);
-
-  show(Math.floor(Math.random() * facts.length)); // a different fact on each visit
-  start();
+  document.getElementById("dykPrev").addEventListener("click", () => show(page - 1));
+  document.getElementById("dykNext").addEventListener("click", () => show(page + 1));
+  show(0);
 </script>

src/lib/facts.js +79 −28
@@ -1,7 +1,11 @@
-// "Did you know?" facts for the home page.
-// The first group is computed from the scores, so it stays true when the data changes.
-// The second group is curated from the country write-ups; keep each one in step with
-// the matching file in /countries.
-import { SEGMENTS, countries, regions, segAvg, worldAvg } from "./countries.js";
+// "Did you know?" on the home page: short curious cases, each linked into the site.
+// Three groups: computed from the scores (so they stay true when the data changes), curated
+// from the country write-ups, and drawn from the history in /changes.yaml. Keep the curated
+// and history ones in step with the files they come from.
+//
+// Links are written inline: [[DE|Germany]] opens a country, [[h:canada-ban-1923|banned it]]
+// opens a history entry. `kicker` is the small label above the fact; `history` adds a
+// "the full story" link to that entry. Unknown countries or entries stop the build.
+import { SEGMENTS, countries, changes, regions, segAvg, worldAvg } from "./countries.js";
 
 const seg = (id) => SEGMENTS.findIndex((s) => s.id === id);
@@ -9,4 +13,5 @@ const P = seg("possession"), C = seg("cultivation"), S = seg("sharing"), A = seg
 const list = (names) => names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0];
 const byId = (id) => countries.find((c) => c.id === id);
+const link = (c) => `[[${c.id}|${c.name}]]`;
 
 function computed() {
@@ -25,33 +30,79 @@ function computed() {
 
   return [
-    { text: `The best score in the world is only ${top.score} out of 100. ${top.name} leads the index, but its caps on how much you may carry and grow keep it barely past the halfway mark.`, id: top.id },
-    { text: `${zero.length} countries score exactly zero: ${list(zero.map((c) => c.name))}. In each, even a small amount means prison or corporal punishment.` },
-    { text: `Growing a few plants at home is legal in just ${growers.length} of ${countries.length} countries.` },
-    { text: `Legal to have, nowhere to buy: ${noShop.length} countries let adults possess cannabis but offer no real legal way to obtain it — among them ${list(noShop.slice(0, 4).map((c) => c.name))}.`, id: noShop[0]?.id },
-    { text: `${SEGMENTS[lowest].name} is the rarest freedom. The world average is ${segAvg[lowest].toFixed(1)} out of 10, lower than for any other segment.` },
-    { text: `${medOnly.length} countries allow some medical use of cannabis while keeping recreational use fully illegal.` },
-    { text: `${regionAvg[0].r} is the freest region, averaging ${Math.round(regionAvg[0].avg)}. ${regionAvg.at(-1).r} is the strictest at ${Math.round(regionAvg.at(-1).avg)}. The world average is ${worldAvg.toFixed(1)}.` },
+    { kicker: "The ceiling", text: `The best score in the world is only ${top.score} out of 100. ${link(top)} leads the index, but its caps on how much you may carry and grow keep it barely past the halfway mark.` },
+    { kicker: "The floor", text: `${zero.length} countries score exactly zero: ${list(zero.map(link))}. In each, even a small amount means prison or corporal punishment.` },
+    { kicker: "Home growing", text: `Growing a few plants at home is legal in just ${growers.length} of ${countries.length} countries.` },
+    { kicker: "Legal to have, nowhere to buy", text: `${noShop.length} countries let adults possess cannabis but offer no real legal way to obtain it — among them ${list(noShop.slice(0, 4).map(link))}.` },
+    { kicker: "The rarest freedom", text: `${SEGMENTS[lowest].name} is the rarest freedom. The world average is ${segAvg[lowest].toFixed(1)} out of 10, lower than for any other segment.` },
+    { kicker: "Medicine only", text: `${medOnly.length} countries allow some medical use of cannabis while keeping recreational use fully illegal.` },
+    { kicker: "By region", text: `${regionAvg[0].r} is the freest region, averaging ${Math.round(regionAvg[0].avg)}. ${regionAvg.at(-1).r} is the strictest at ${Math.round(regionAvg.at(-1).avg)}. The world average is ${worldAvg.toFixed(1)}.` },
   ];
 }
 
 const curated = [
-  { id: "DE", text: "In Germany you may grow three plants and carry 25 g — but giving any of it to a friend, even for free, is still a crime. That is why it scores 1 out of 10 for sharing." },
-  { id: "SG", text: "Singapore prosecutes its citizens and permanent residents for using cannabis abroad, even in countries where it is legal, and can test them when they return." },
-  { id: "UY", text: "Uruguay was the first country to legalise, in 2013. Buyers register with the state and are verified by fingerprint at the pharmacy; tourists are not allowed to buy." },
-  { id: "KM", text: "Cannabis was fully legal in the Comoros from 1975 to 1978 under President Ali Soilih. It has been banned ever since." },
-  { id: "JP", text: "Until December 2024, Japan punished possessing cannabis but not using it. The law now makes use itself a crime, with up to seven years in prison." },
-  { id: "KP", text: "North Korea is often called a cannabis paradise online. Its penal code actually lists cannabis alongside cocaine and heroin; the myth probably comes from legal hemp and a rough local tobacco." },
-  { id: "CO", text: "Colombia lets adults grow up to 20 plants with no licence or registration — one of the most generous home-grow allowances anywhere — yet has no legal shops." },
-  { id: "CH", text: "Switzerland has an unusual sharing rule: handing up to 10 g to another adult for free, to consume together, is not punishable." },
-  { id: "PW", text: "In Palau, giving away less than an ounce is punished like simple possession, with a fine, rather than as trafficking." },
-  { id: "FJ", text: "Fiji's Court of Appeal has told judges not to jail people for up to 100 g of cannabis: fines, community service or a warning instead." },
-  { id: "NR", text: "Nauru, population about 12,000, has the harshest rule in the Pacific islands: a mandatory minimum of 12 months in prison for possessing any illicit drug." },
-  { id: "IN", text: "In India the flowers and resin are banned, but bhang, made from cannabis leaves, is legal and sold in government-licensed shops in several states." },
-  { id: "TH", text: "Thailand went from thousands of open dispensaries to prescription-only in three years. Since June 2025 cannabis flower officially requires a prescription." },
-  { id: "MT", text: "Malta was the first EU country to legalise, in 2021 — but there are no shops, only non-profit associations capped at 500 members each." },
+  { kicker: "Germany", text: "In [[DE|Germany]] you may grow three plants and carry 25 g — but giving any of it to a friend, even for free, is still a crime. That is why it scores 1 out of 10 for sharing.", history: "germany-cannabis-act-2024" },
+  { kicker: "Singapore", text: "[[SG|Singapore]] prosecutes its citizens and permanent residents for using cannabis abroad, even in countries where it is legal, and can test them when they return." },
+  { kicker: "Uruguay", text: "[[UY|Uruguay]] was [[h:uruguay-legalisation-2013|the first country to legalise]], in 2013. Buyers register with the state and are verified by fingerprint at the pharmacy; tourists are not allowed to buy." },
+  { kicker: "Japan", text: "Until December 2024, [[JP|Japan]] punished possessing cannabis but not using it. The law now [[h:japan-use-criminalised-2024|makes use itself a crime]], with up to seven years in prison for possession." },
+  { kicker: "North Korea", text: "[[KP|North Korea]] is often called a cannabis paradise online. Its penal code actually lists cannabis alongside cocaine and heroin; the myth probably comes from legal hemp and a rough local tobacco." },
+  { kicker: "Colombia", text: "[[CO|Colombia]] lets adults grow up to 20 plants with no licence or registration — one of the most generous home-grow allowances anywhere — yet has no legal shops.", history: "colombia-twenty-plants-2015" },
+  { kicker: "Switzerland", text: "[[CH|Switzerland]] has an unusual sharing rule: handing up to 10 g to another adult for free, to consume together, is not punishable. And since 2023 police [[h:switzerland-possession-legal-2023|may not even confiscate]] that amount." },
+  { kicker: "Palau", text: "In [[PW|Palau]], giving away less than an ounce is punished like simple possession, with a fine, rather than as trafficking." },
+  { kicker: "Fiji", text: "[[FJ|Fiji]]'s Court of Appeal has told judges not to jail people for up to 100 g of cannabis: fines, community service or a warning instead." },
+  { kicker: "Nauru", text: "[[NR|Nauru]], population about 12,000, has the harshest rule in the Pacific islands: a mandatory minimum of 12 months in prison for possessing any illicit drug." },
+  { kicker: "India", text: "In [[IN|India]] the flowers and resin are banned, but bhang, made from cannabis leaves, is legal and sold in government-licensed shops in several states." },
+  { kicker: "Malta", text: "[[MT|Malta]] was [[h:malta-legalisation-2021|the first EU country to legalise]], in 2021 — but there are no shops, only non-profit associations capped at 500 members each." },
 ];
 
+// Curious cases from the history. Each one restates its entry in /changes.yaml.
+const fromHistory = [
+  { kicker: "1787 · Madagascar", history: "madagascar-merina-ban-1787", text: "One of the earliest national bans on record is from [[MG|Madagascar]]: in 1787 King Andrianampoinimerina prohibited cannabis across the Merina kingdom, with death as the penalty." },
+  { kicker: "1798 · India", history: "india-bhang-tax-1798", text: "In 1798 the British Parliament taxed bhang, ganja and charas in [[IN|India]], saying the aim was to reduce consumption. Attempts to make cannabis a crime in 1838, 1871 and 1877 were never enacted." },
+  { kicker: "1800 · Egypt", history: "egypt-french-ban-1800", text: "In October 1800 the French army occupying [[EG|Egypt]] banned hashish, alarmed by its use among its own troops." },
+  { kicker: "1874 · Egypt", history: "egypt-import-tax-1874", text: "[[EG|Egypt]] forbade importing cannabis in [[h:egypt-first-ban-1868|1868]]. Six years later it put a tax on those same imports." },
+  { kicker: "1830 · Brazil", history: "brazil-rio-pango-ban-1830", text: "Rio de Janeiro's council banned cannabis in 1830. A seller paid a fine of 20 milreis; a slave or anyone else caught using it faced up to three days in prison. [[BR|Brazil]]'s other councils followed." },
+  { kicker: "1894 · India", history: "india-hemp-drugs-commission-1894", text: "After hearing nearly 1,200 witnesses, the Indian Hemp Drugs Commission concluded in 1894 that moderate use does practically no harm and that a ban would be unjustified. [[IN|India]] taxed and licensed cannabis until 1985." },
+  { kicker: "1915 · Trinidad", history: "trinidad-ganja-licensing-1915", text: "[[TT|Trinidad and Tobago]] set up licensed ganja sellers in 1915. Ten years later it [[h:trinidad-ganja-ordinance-1925|banned the plant outright]]." },
+  { kicker: "1923 · Canada", history: "canada-ban-1923", text: `[[CA|Canada]]${countries[0].id === "CA" ? ", today's highest-scoring country," : ""} banned cannabis in 1923 without a word of debate in Parliament. Nobody has established why: there was almost no use, and the first seizure came nine years later.` },
+  { kicker: "1923 · South Africa", history: "south-africa-league-request-1923", text: `It was [[ZA|South Africa]] that asked the League of Nations in 1923 to treat cannabis like opium and cocaine. A century later it ranks #${byId("ZA")?.rank} in the world for cannabis freedom.` },
+  { kicker: "1934 · China", history: "china-xinjiang-hashish-trade-ended-1934", text: "Until 1934 hashish was exported legally from Xinjiang in [[CN|China]] to British India, under a tariff." },
+  { kicker: "1937 · Uruguay", history: "uruguay-narcotics-law-1937", text: "[[UY|Uruguay]] made hashish a state monopoly sold only by pharmacies in 1937. Eighty years later its pharmacies [[h:uruguay-pharmacy-sales-2017|started selling cannabis again]] — this time without a prescription." },
+  { kicker: "1940 · Mexico", history: "mexico-brief-legalisation-1940", text: "For five months in 1940 [[MX|Mexico]] let doctors prescribe drugs to users through state dispensaries. The United States cut off its exports of medical narcotics, and Mexico [[h:mexico-regulation-suspended-1940|suspended the rule]]." },
+  { kicker: "1944 · United States", history: "us-la-guardia-report-1944", text: "A five-year study ordered by the mayor of New York found in 1944 that marijuana did not lead to addiction, crime or harder drugs. The federal narcotics bureau attacked it, and [[US|US]] law did not change." },
+  { kicker: "1953 · Netherlands", history: "netherlands-possession-ban-1953", text: "The [[NL|Netherlands]] only made possessing cannabis an offence in 1953 — just over twenty years before it [[h:netherlands-toleration-1976|became known for tolerating it]]." },
+  { kicker: "1966 · Nigeria", history: "nigeria-indian-hemp-decree-1966", text: "[[NG|Nigeria]]'s 1966 Indian Hemp Decree set death or 21 years for growing cannabis and at least ten years for possessing it. The death penalty [[h:nigeria-death-penalty-removed-1975|was dropped in 1975]]." },
+  { kicker: "1973 · Nepal", history: "nepal-shops-closed-1973", text: "Kathmandu had licensed cannabis shops until 16 July 1973, when [[NP|Nepal]] cancelled every licence under pressure from the United States and the United Nations." },
+  { kicker: "1975 · Comoros", history: "comoros-legalisation-1975", text: "Cannabis was legal in the [[KM|Comoros]] for three years: President Ali Soilih lifted the ban in 1975, and the government that overthrew him [[h:comoros-ban-restored-1978|restored it in 1978]]." },
+  { kicker: "1988 · United States", history: "us-dea-judge-young-1988", text: "In 1988 the drug agency's own judge in the [[US|United States]] ruled that cannabis should be rescheduled, calling it one of the safest therapeutically active substances known. The agency's head rejected the ruling." },
+  { kicker: "1995 · Nepal", history: "nepal-pashupati-distribution-ended-1995", text: "Until 1995 the trust that runs [[NP|Nepal]]'s Pashupatinath temple handed holy men marijuana at the Shivaratri festival — taken from the Home Ministry's confiscated stock." },
+  { kicker: "2003 · Portugal", history: "portugal-seeds-law-2003", text: "[[PT|Portugal]]'s drug law controls one very specific thing: since 2003 its list has included \"cannabis seeds not intended for sowing\"." },
+  { kicker: "2012 · Italy", history: "italy-seeds-ruling-2012", text: "[[IT|Italy]]'s top court ruled in 2012 that selling cannabis seeds is not a crime, because what the buyer will do with them cannot be known." },
+  { kicker: "2017 · Indonesia", history: "indonesia-fidelis-case-2017", text: "In [[ID|Indonesia]] a man was jailed for eight months in 2017 for growing cannabis to treat his wife. She died while he was detained." },
+  { kicker: "2022 · Russia", history: "russia-griner-sentence-2022", text: "[[RU|Russia]] sentenced an American basketball player to nine years in 2022 for 0.7 grams of cannabis oil. She was freed in a prisoner exchange that December." },
+  { kicker: "2022–2025 · Thailand", history: "thailand-prescription-only-2025", text: "[[TH|Thailand]] went from [[h:thailand-decriminalisation-2022|thousands of open dispensaries]] to prescription-only in three years. Since June 2025 cannabis flower officially requires a prescription." },
+  { kicker: "2024 · Morocco", history: "morocco-royal-pardon-2024", text: "[[MA|Morocco]]'s sultan [[h:morocco-hassan-regulations-1890|licensed tribes of the Rif to grow kif]] in 1890. In 2024 its king pardoned 4,831 people convicted, prosecuted or wanted over growing it." },
+];
+
+// Turn the inline [[…|…]] links into parts the component can render, checking each target.
+function parse(text) {
+  return text.split(/(\[\[[^\]]+\]\])/).filter(Boolean).map((part) => {
+    const m = part.match(/^\[\[([^|]+)\|([^\]]+)\]\]$/);
+    if (!m) return part;
+    if (m[1].startsWith("h:")) {
+      const change = changes.find((ch) => ch.id === m[1].slice(2));
+      if (!change) throw new Error(`facts.js: unknown history entry "${m[1].slice(2)}"`);
+      return { label: m[2], change };
+    }
+    const country = byId(m[1]);
+    if (!country) throw new Error(`facts.js: unknown country "${m[1]}"`);
+    return { label: m[2], country };
+  });
+}
+
 export function getFacts() {
-  const facts = [...computed(), ...curated].filter((f) => !f.id || byId(f.id));
-  return facts.map((f) => ({ ...f, country: f.id ? byId(f.id) : null }));
+  return [...fromHistory, ...curated, ...computed()].map((f) => {
+    const history = f.history ? changes.find((ch) => ch.id === f.history) : null;
+    if (f.history && !history) throw new Error(`facts.js: unknown history entry "${f.history}"`);
+    return { kicker: f.kicker, parts: parse(f.text), history };
+  });
 }

src/pages/index.astro +34 −10
@@ -5,5 +5,5 @@ import Changes from "../components/Changes.astro";
 import AverageChart from "../components/AverageChart.astro";
 import { SEGMENTS, DATA_SNAPSHOT, REC_LABEL, REC_ORDER, REC_HELP, MED_LABEL, MED_HELP, countries, regions, worldAvg, lastUpdated, formatDate, subunitsOf, places, trendOf, changeDate, TREND_YEARS } from "../lib/countries.js";
-import { TIERS, tierColor } from "../lib/tiers.js";
+import { TIERS, tierColor, tierOf } from "../lib/tiers.js";
 import { PLANTING_STATUS, PLANTING_TRADE, STATUS_SECTIONS } from "../lib/segment-ids.js";
 import { PRICE_MARKETS, PRICE_MARKET_LABEL, flatLatestPricesFor, latestPricesFor } from "../lib/prices.js";
@@ -119,4 +119,25 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
         <AverageChart />
       </div>
+      {/* The two ends of the ranking, side by side: the leader of each list is set largest. */}
+      <div class="extremes">
+        {[["Freest", countries.slice(0, 5)], ["Least free", countries.slice(-5).reverse()]].map(([label, list]) => (
+          <section aria-label={label}>
+            <h2>{label}</h2>
+            <ol>
+              {list.map((c) => (
+                <li>
+                  <a href={`${base}country/${c.slug}/`} data-country={c.id} style={`--c:${tierColor(c.score)}`}>
+                    <span class="ext-rank">{String(c.rank).padStart(2, "0")}</span>
+                    <span class="ext-name"><span class="flag">{c.flag}</span>{c.name}</span>
+                    <span class="ext-tier">{tierOf(c.raw).name}</span>
+                    <b>{c.score}</b>
+                  </a>
+                </li>
+              ))}
+            </ol>
+          </section>
+        ))}
+      </div>
+      <DidYouKnow />
       <Changes />
     </section>
@@ -299,14 +320,16 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
         </div>
       </div>
-      <div class="card note-card tomato">
-        <h3>🍅 The tomato standard</h3>
-        <p>A 10 means no cannabis-specific rule at all: carry any amount, grow a whole field, give it away, buy it anywhere. Every cap, licence, registry and restricted zone costs points, because regulation is counted as unfreedom. Canada lets you carry 30 g and grow four plants — that is legal, but it is not unlimited, so it scores 6 and 4, not 9 and 8. This is why even the most liberal countries land around 50 and the “Free” tier is empty.</p>
+      {/* The rule behind every score, said large; then how the 100 points are divided. */}
+      <div class="tomato">
+        <p class="tomato-label">🍅 The tomato standard</p>
+        <p class="tomato-big">A 10 means <em>no cannabis-specific rule at all</em>: carry any amount, grow a whole field, give it away, buy it anywhere.</p>
+        <p>Every cap, licence, registry and restricted zone costs points, because regulation is counted as unfreedom. Canada lets you carry 30 g and grow four plants — that is legal, but it is not unlimited, so it scores 6 and 4, not 9 and 8. This is why even the most liberal countries land around 50 and the “Free” tier is empty.</p>
       </div>
       <div class="weights" aria-hidden="true">
-        {SEGMENTS.map((s) => <div style={`flex:${s.weight}`} title={`${s.name} — ${s.weight}%`}>{s.weight}%</div>)}
+        {SEGMENTS.map((s) => <div style={`flex:${s.weight}`} title={`${s.name} — ${s.weight}%`}><b>{s.weight}</b><span>{s.name}</span></div>)}
       </div>
       <div class="seg-grid">
         {SEGMENTS.map((s) => (
-          <div class="card seg">
+          <div class="seg">
             <div class="seg-top"><h3>{s.name}</h3><span class="seg-w">{s.weight}%</span></div>
             <p>{s.desc}</p>
@@ -315,5 +338,5 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
         ))}
       </div>
-      <div class="card note-card">
+      <div class="note-card">
         <h3>Read this before relying on it</h3>
         <p>Scores are editorial estimates that blend the written law with how it is enforced in practice. Federal countries (United States, Australia, India) get a single national estimate that hides large internal differences. Scores between the listed levels mean a country sits between two descriptions. Every country has a written explanation and linked sources for each segment — select a country to read them. For many smaller countries the only sources available are secondary summaries, and the explanations say so where a figure could not be confirmed.</p>
@@ -322,6 +345,4 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
     </section>
 
-    <DidYouKnow />
-
   </main>
 
@@ -672,5 +693,8 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
       const open = Object.assign(document.createElement("a"), { className: "btn btn-primary drawer-open", href: url });
       open.innerHTML = 'Open full page <span aria-hidden="true">→</span>';
-      (article.querySelector(".c-disputed") ?? article.querySelector(".d-note")).after(open);
+      // after the summary and the status chips, just above "In short"
+      const short = article.querySelector(".c-top > .d-h");
+      if (short) short.before(open);
+      else (article.querySelector(".c-disputed") ?? article.querySelector(".d-note")).after(open);
       return article;
     }

src/styles/global.css +135 −40
@@ -220,22 +220,36 @@ th.num-col { text-align: right; }
 
 /* ── Methodology ────────────────────────────────────── */
-.weights { display: flex; gap: 2px; height: 34px; margin-bottom: 16px; }
-.weights div {
-  background: var(--accent); color: var(--accent-ink); font-size: 12px; font-weight: 600;
-  display: grid; place-items: center; overflow: hidden;
-}
-.weights div:first-child { border-radius: var(--r); }
-.weights div:last-child { border-radius: 0 8px 8px 0; }
-.seg-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
-.seg { padding: 18px 20px; }
+/* the rule, said large, with the reasoning beside it */
+.tomato { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 12px clamp(32px, 6vw, 96px); align-items: end; padding-top: 14px; border-top: 1px solid var(--text); }
+.tomato-label { grid-column: 1 / -1; margin: 0 0 6px; font: 400 11.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; }
+.tomato-big { margin: 0; font: 400 clamp(28px, 3.6vw, 52px)/1.04 var(--display); letter-spacing: -.015em; text-wrap: balance; }
+.tomato p:last-child { margin: 0; color: var(--text-2); max-width: 52ch; }
+/* how the 100 points divide: one ruler, each stretch as long as its weight */
+.weights { display: flex; gap: 6px; margin: clamp(40px, 6vw, 72px) 0 0; }
+.weights div { min-width: 0; padding-top: 8px; border-top: 4px solid var(--text); display: flex; flex-direction: column; gap: 2px; }
+.weights b { font: 400 clamp(20px, 2.4vw, 32px)/1 var(--mono); letter-spacing: -.05em; }
+.weights span { font: 400 10.5px/1.3 var(--mono); text-transform: uppercase; letter-spacing: .05em; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+/* the six segments: three across, flush left, ruled */
+.seg-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 44px clamp(28px, 4vw, 56px); margin-top: clamp(36px, 5vw, 60px); }
+.seg { padding: 12px 0 0; border-top: 1px solid var(--border); }
 .seg-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
-.seg-w { font-family: var(--display); font-weight: 700; font-size: 22px; color: var(--accent); }
-.seg p { color: var(--text-2); font-size: 13.5px; }
-.anchors { list-style: none; margin: 0; padding: 0; font-size: 13px; color: var(--text-2); }
-.anchors li { display: flex; gap: 10px; padding: 3px 0; }
-.anchors b { width: 20px; flex: none; text-align: right; color: var(--text); font-variant-numeric: tabular-nums; }
-.note-card { padding: 22px 24px; margin-top: 16px; }
+.seg-top h3 { font-size: 26px; line-height: 1.1; margin: 0; }
+.seg-w { font: 400 13px var(--mono); color: var(--muted); }
+.seg p { color: var(--text-2); font-size: 14px; margin: 8px 0 14px; max-width: 40ch; }
+.anchors { list-style: none; margin: 0; padding: 0; font-size: 13.5px; color: var(--text-2); }
+.anchors li { display: grid; grid-template-columns: 2ch minmax(0, 1fr); gap: 12px; padding: 5px 0; border-top: 1px dotted var(--border); }
+.anchors b { text-align: right; color: var(--text); font: 500 12.5px/1.6 var(--mono); }
+.note-card { margin-top: clamp(40px, 6vw, 72px); padding-top: 14px; border-top: 1px solid var(--text); }
+.note-card h3 { margin-bottom: 10px; }
 .note-card p { color: var(--text-2); max-width: 80ch; }
 .note-card p:last-child { margin: 0; }
+@media (max-width: 900px) {
+  .tomato { grid-template-columns: 1fr; }
+  .seg-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+@media (max-width: 600px) {
+  .seg-grid { grid-template-columns: 1fr; }
+  .weights span { display: none; }
+}
 
 .footer { margin-top: 56px; padding: 26px 16px 40px; text-align: center; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
@@ -264,5 +278,5 @@ th.num-col { text-align: right; }
 .drawer-inner { padding: 22px 24px 40px; }
 .drawer-inner .d-head { padding-right: 92px; }
-.drawer-inner .drawer-open { display: flex; justify-content: center; padding: 12px 16px; font-size: 15px; margin: 16px 0 4px; }
+.drawer-inner .drawer-open { display: flex; justify-content: center; padding: 12px 16px; font-size: 15px; margin: 22px 0 0; }
 .drawer-inner .c-sources { display: none; }
 /* Shown while the article downloads; fades in late so a fast load never flashes it. */
@@ -346,5 +360,4 @@ a.chip:hover { border-color: var(--muted); color: var(--text); }
 .sub-head { margin-bottom: 12px; }
 .sub-head h3 { margin: 0; }
-.tomato { margin: 0 0 18px; }
 
 /* ── Country page ───────────────────────────────────── */
@@ -353,14 +366,6 @@ a.chip:hover { border-color: var(--muted); color: var(--text); }
 .back { color: var(--text-2); font-weight: 500; }
 .back:hover { color: var(--text); }
-.country-page .country { display: grid; grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); gap: 20px 56px; align-items: start; }
-.country-page .c-side { position: sticky; top: 84px; }
-.country-page .d-head h1 { font-size: clamp(30px, 4vw, 44px); }
-.country-page .d-flag { font-size: 56px; }
 .country-page .c-main > .d-h:first-child { margin-top: 0; }
 /* Score over time: beside the head in the drawer, under the score breakdown on the full page */
-.chart-in-main { display: none; }
-.country-page .chart-in-side { display: none; }
-.country-page .chart-in-main { display: block; margin-top: 26px; }
-.country-page .chart-in-main .c-chart { margin-top: 0; }
 .country-page .why p, .country-page .ctx { font-size: 15px; max-width: 72ch; }
 .c-sources ol { margin: 6px 0 0; padding-left: 20px; color: var(--muted); font-size: 13.5px; }
@@ -375,6 +380,4 @@ a.chip:hover { border-color: var(--muted); color: var(--text); }
 .pager span { font-weight: 600; }
 @media (max-width: 860px) {
-  .country-page .country { grid-template-columns: 1fr; }
-  .country-page .c-side { position: static; }
 }
 
@@ -435,15 +438,4 @@ a.chip:hover { border-color: var(--muted); color: var(--text); }
 
 /* ── Did you know? ──────────────────────────────────── */
-.dyk { margin-top: 48px; padding: 20px 24px 22px; border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
-.dyk-top { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
-.dyk-label { margin: 0; color: var(--accent); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: .09em; }
-.dyk-nav { display: flex; align-items: center; gap: 6px; }
-.dyk-nav .icon-btn { width: 30px; height: 30px; font-size: 18px; }
-.dyk-count { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; margin-right: 6px; }
-.dyk-facts { min-height: 104px; }
-.dyk-fact p { font-family: var(--display); font-size: clamp(17px, 2vw, 21px); line-height: 1.4; margin: 0 0 10px; max-width: 62ch; letter-spacing: -.01em; }
-.dyk-fact a { color: var(--accent); font-weight: 500; font-size: 14px; }
-.dyk-fact a:hover { text-decoration: underline; text-underline-offset: 3px; }
-@media (max-width: 640px) { .dyk-facts { min-height: 170px; } }
 
 /* ── States, provinces and territories ──────────────── */
@@ -794,8 +786,8 @@ h3 { font-size: 22px; }
 .brand, .big { font-family: var(--mono); letter-spacing: 0; }
 /* figures: monospace, never the serif */
-.ring text, .seg-w, .cmp-score b, .c-use dd, .hs-grid dd, .avg-now b, .score-val, td.num, .num-col, .pill, .delta, .trend, .c-chart-val, .avg-y, .avg-x, time {
+.ring text, .cmp-score b, .c-use dd, .hs-grid dd, .avg-now b, .score-val, td.num, .num-col, .pill, .delta, .trend, .c-chart-val, .avg-y, .avg-x, time {
   font-family: var(--mono); font-weight: 500; letter-spacing: -.02em;
 }
-.d-score .tier, .ca-score .tier, .cmp-head a, .ca-head b, .dyk-fact p { font-family: var(--display); font-weight: 400; letter-spacing: 0; }
+.d-score .tier, .ca-score .tier, .cmp-head a, .ca-head b { font-family: var(--display); font-weight: 400; letter-spacing: 0; }
 .d-score .tier { font-size: 26px; line-height: 1.1; }
 /* small labels: monospace capitals */
@@ -849,4 +841,40 @@ h3 { font-size: 22px; }
 .avg-plot .line { stroke-width: 1.5; }
 .avg-plot .grid { stroke-dasharray: 1 4; }
+/* best and worst: two ruled lists, the leader of each set largest */
+.extremes { display: grid; grid-template-columns: 1fr 1fr; gap: 40px clamp(32px, 6vw, 96px); margin-top: clamp(44px, 7vw, 88px); }
+.extremes section { border-top: 1px solid var(--text); }
+.extremes h2 { font: 400 11.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; margin: 0; padding: 12px 0; }
+.extremes ol { list-style: none; margin: 0; padding: 0; }
+.extremes a { display: grid; grid-template-columns: 3ch minmax(0, 1fr) auto 4ch; gap: 0 16px; align-items: baseline; padding: 10px 0; border-top: 1px solid var(--border); }
+.ext-rank { font: 400 12px var(--mono); color: var(--muted); }
+.ext-name { font: 400 clamp(20px, 2.2vw, 28px)/1.1 var(--display); display: flex; align-items: baseline; gap: 12px; min-width: 0; }
+.ext-name .flag { font-size: .7em; }
+.ext-tier { font: 400 10.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); white-space: nowrap; }
+.extremes b { font: 500 15px var(--mono); text-align: right; justify-self: end; padding-bottom: 2px; border-bottom: 2px solid var(--c); }
+.extremes li:first-child a { padding: 18px 0 16px; grid-template-columns: 3ch minmax(0, 1fr) auto; }
+/* the leader's name is too big to share a line with its tier label: the label goes underneath */
+.extremes li:first-child .ext-tier { grid-column: 2; grid-row: 2; margin-top: 10px; }
+.extremes li:first-child .ext-name { overflow-wrap: anywhere; }
+.extremes li:first-child .ext-name { font-size: clamp(40px, 5.6vw, 84px); line-height: .95; letter-spacing: -.02em; }
+.extremes li:first-child b { font-size: clamp(22px, 2.4vw, 32px); font-weight: 400; letter-spacing: -.04em; }
+.extremes a:hover .ext-name { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 5px; }
+@media (max-width: 900px) { .extremes { grid-template-columns: 1fr; } .ext-tier, .extremes li:first-child .ext-tier { display: none; } .extremes a { grid-template-columns: 3ch minmax(0, 1fr) 4ch; } }
+/* did you know: three curious cases at a time, every name a link */
+.dyk { margin-top: clamp(44px, 7vw, 88px); border-top: 1px solid var(--text); }
+.dyk-top { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 12px 0; }
+.dyk-top h2 { font: 400 11.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; margin: 0; }
+.dyk-nav { display: flex; align-items: baseline; gap: 14px; font: 400 11.5px var(--mono); color: var(--muted); }
+.dyk-nav button { padding: 0 2px; border: 0; background: none; color: var(--text-2); font: 400 15px var(--mono); cursor: pointer; }
+.dyk-nav button:hover { color: var(--text); }
+.dyk-facts { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px clamp(28px, 4vw, 56px); border-top: 1px solid var(--border); min-height: 15em; }
+.dyk-fact { padding-top: 16px; display: flex; flex-direction: column; gap: 12px; }
+.dyk-fact[hidden] { display: none; }
+.dyk-kicker { margin: 0; font: 400 10.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
+.dyk-text { margin: 0; font: 400 clamp(20px, 2vw, 26px)/1.18 var(--display); text-wrap: pretty; }
+.dyk-text a { text-decoration: underline; text-decoration-thickness: 1px; text-decoration-color: var(--muted); text-underline-offset: 4px; }
+.dyk-text a:hover { text-decoration-color: currentColor; }
+.dyk-more { margin-top: auto; font: 400 10.5px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-2); }
+.dyk-more:hover { color: var(--text); }
+@media (max-width: 900px) { .dyk-facts { grid-template-columns: 1fr; min-height: 0; } }
 /* latest changes: a ledger */
 .ledger { margin-top: clamp(44px, 7vw, 88px); border-top: 1px solid var(--text); }
@@ -903,2 +931,69 @@ tbody tr:hover .cname { text-decoration: underline; text-underline-offset: 4px;
 
 .footer { text-align: left; font-size: 10.5px; padding: 28px clamp(16px, 4vw, 40px) 48px; margin-top: clamp(64px, 9vw, 120px); border-top: 1px solid var(--text); }
+
+/* ══ Country profile ═════════════════════════════════════════════════════════
… 66 more lines

    ↑↓ to move, ↵ to open.