Cannabis Freedom Index

Changelog 3ff3e49

latest changes

3 history entries added1 score changed7 country profiles edited11 files of code, design or docs +429 −61 lines

Score changes 1 place

  1. Grenada1137+26possession 1 → 5cultivation 1 → 4enforcement 2 → 5consumption 1 → 3

History changes.yaml

Added 3

Countries and states 8 files

Grenada countries/gd.yaml

possession 1 → 5cultivation 1 → 4enforcement 2 → 5consumption 1 → 3

recreational

illegallegal

summary

Reform towards decriminalisationSince February 2026 adults 21 and over may possess up to 56 g and grow four plants per household. There is no legal market yet; a medicalcommercial framework has been under wayis planned as a second phase.

segments › possession › text

Possession is still a criminal offence underParliament passed the Drug Abuse (Prevention and Control) (Amendment) Act, in January 2026 with finessupport from both sides, and possible prison. In 2025 the government announced a billGovernor-General assented to depenalise small quantities; an earlier 2021 draft used 28it in February. Adults aged 21 and over may possess up to 56 g as the limitof cannabis and 15 g of resin. Scored on the law in force, as passage could not be confirmedLarger amounts remain an offence.

segments › possession › sources

ng1vino, ng2act

segments › cultivation › text

Illegal. The 2021 draft would have let each adult householder grow upUp to fivefour plants per household for private use. Rastafari places of worship have their own cultivation allowance.

segments › cultivation › sources

ng2vino, jong3

segments › enforcement › text

Arrests and prosecutions for small amounts continue pending reform — one ofNothing within the last eastern Caribbean states without decriminalisationlimits. Pending cases over minor amounts are discontinued; larger quantities and dealing remain criminal.

segments › enforcement › sources

jovino, ng2ng3

segments › sharing › text

Supply is traffickingThe 2026 Act covers personal possession and growing; supplying others remains an offence, apart from sacramental use within registered Rastafari places of worship.

segments › sharing › sources

ng1vino

segments › consumption › text

UsePrivate use is an offencelawful. The planned reform would permit Rastafari sacramental use at places of worshipPublic consumption remains prohibited and is punished with a fixed penalty.

segments › consumption › sources

ng1, jovino

segments › access › text

No legal source besides home growing. The government describes this as phase one; a regulatory framework for commercial cultivation and sale is being drafted as phase two.

segments › access › sources

jovino, ng3

context › production › text

No licensed production yet; the bill envisages medicinala national cannabis policy and scientific usesupporting legislation for a commercial sector are in preparation.

context › production › sources

ng1vino

context › products › text

No legal products on sale.

context › products › sources

jovino

context › medical › text

No medical programme yet; it is expected as part of the bill would decriminalise medicinal and therapeutic usesecond phase.

context › medical › sources

ng1ng3

context › consequences › text

Convictions carry aThe Act provides amnesty and automatic expungement of criminal recordrecords for specified minor cannabis offences. The minimum age is 21, after the prime minister had first proposed 18.

context › consequences › sources

jovino, ng3

sources

Virgin Islands News Online — Grenadian parliament approves amendment to marijuana legislation (January 2026) Parliament of Grenada — Drug Abuse (Prevention and Control) (Amendment) Act, No. 1 of 2026 (PDF) NOW Grenada — Draft bill and policy statement for the decriminalisation of cannabis in Grenada (January 2026) 3 unchanged

Germany countries/de.yaml

context › medical › text

Cannabis is no longer a narcotic in law: any doctor can prescribe it on an ordinary prescription, dispensed by pharmacies, and statutory insurers cover it for serious illness after approval. Still prescriptionA government bill to require an in-only, and rules on remoteperson consultation before prescribing have been tightened, aimed at telemedicine platforms, went through parliamentary hearings in January 2026; whether it has taken effect could not be confirmed.

context › medical › sources

medcang, bmg, cansativa

sources

Cansativa — Political outlook: MedCanG moves through parliament (2026) 3 unchanged

Jamaica countries/jm.yaml

context › production › text

The Cannabis Licensing Authority issues cultivator, processor, transporter, retail and research licences in tiers by size. Fees, security rules and banking problems have kept many traditional small growers outside the legal industry; in 2026 the authority widened fee-free routes in for them, with two-year transition periods and collective cultivation of up to ten acres for community groups.

context › production › sources

cla, mh

sources

The Marijuana Herald — Daily cannabis news brief, 29 September 2026 3 unchanged

Thailand countries/th.yaml

segments › access › text

Shops still exist, but from 2026 only medical facilities, pharmacies, herbal-product retai … n. Of more than 18,000 registered outlets, over 7,000 closed during 2025 rather than renew, and authorities are moving against roughly 6,000 unlicensed or expired outlets. A draft Cannabis and Hemp Act that would restrict cannabis strictly to medical use finished public consultation in June 2026 and awaits parliament.

segments › access › sources

khaosod, thaiger, nation

sources

The Nation — Thailand cannabis draft law targets medical use and shop controls (July 2026) 3 unchanged

Connecticut countries/us/ct.yaml

segments › access

Licensed shops since 2023. From 1 October 2026 the 70% THC cap on concentrates is gone and the potency-based tax is replaced by a flat rate.

sources

The Marijuana Herald — Daily cannabis news brief, 29 September 2026 1 unchanged

United States countries/us/index.yaml

context › production › text

States issue commercial licences, some uncapped (Oklahoma, Oregon), many tightly capped and very expensive. InOn 22 April 2026 the DEA moved FDA-approved and state-licensed medical cannabis was moved to federal Schedule III; adult. Adult-use businesses remaincannabis remains Schedule I, with punitive federal tax treatment and limited banking; a hearing on moving all cannabis to Schedule III ran from 29 June to 15 July 2026 and a decision is pending.

context › production › sources

ml, hh, ncsl

sources

Morgan Lewis — Post-DEA hearing, broader marijuana rescheduling questions remain (September 2026) 4 unchanged

Virginia countries/us/va.yaml

summary

1 ozPossession and four plants per household have been legal since 2021. After years of vetoes, buta retail law was enacted in 2026; adult-use shops were repeatedly vetoedare due to open in 2027. MedicalUntil then medical dispensaries are the only legal place to buy.

segments › access

No adult-use shops: yet. Earlier bills to license them were vetoed, but in 2026 the legislature passed a retail framework and the new governor signed sales into law, with shops due to open during 2027. Medical dispensaries are the only legal place to buy for now.

sources

Virginia Mercury — General Assembly advances cannabis retail framework (February 2026) 1 unchanged

South Africa countries/za.yaml

summary

A 2018 Constitutional Court ruling and the 2024 Cannabis for Private Purposes Act makeprotects adult possession, use and cultivation in private legal, and providethe 2024 Cannabis for expungementPrivate Purposes Act writes that into law. The Act is not yet in force: draft regulations published in February 2026 propose caps of old records750 g and five plants. Buying and selling remain illegal, leaving a grey market of private clubs.

segments › possession › text

Adults 18+ may possess cannabis for private purposes, kept out of public view — at home, or concealed while carrying it. The 2024 Act itself sets no gram limit itself and is not yet in force; maximum quantities are left todraft regulations published on 3 February 2026 propose a cap of 750 g, so for now there is no fixed numberat home or carried concealed in public. TheUntil they are adopted, the right rests on the Constitutional Court's 2018 privacy ruling, with no fixed number.

segments › possession › sources

act, idpcbd

segments › cultivation › text

Adults may grow cannabis in a private place for their own use, with no licence or registration. As with possessionThe February 2026 draft regulations would cap this at five plants per adult, the number of plants is left to regulations rather than fixed in the Actany size or strain. Plants mustThat cap is not be accessible to children or visible to the publicyet law; if adopted, this segment's score would fall.

segments › cultivation › sources

act, adamsbd

sources

Business Day — Proposed rules cap private cannabis possession at 750g (February 2026) 4 unchanged

Code, design and documents 11 files

README.md +1 −0
@@ -39,4 +39,5 @@ Built with [Astro](https://astro.build). Pushing to `main` deploys to GitHub Pag
 | `countries/us/` | A federation: `index.yaml` for the country, one file per state |
 | `segments.yaml` | The six segments, their weights and level descriptions |
+| `changes.yaml` | Laws and events that moved a country's score, with the scores before; the home page shows the latest three |
 | `src/` | The website: pages, components, styles |
 

src/components/ChangeCard.astro +17 −0
@@ -0,0 +1,17 @@
+---
+// A change in short: date, country, title and the overall score before → after.
+// The whole card links to the change's own page.
+import { formatDate } from "../lib/countries.js";
+import ScoreChange from "./ScoreChange.astro";
+
+const { ch } = Astro.props;
+const base = import.meta.env.BASE_URL;
+---
+<a class="card change" href={`${base}changes/${ch.id}/`}>
+  <p class="change-meta">
+    <time datetime={ch.date}>{formatDate(ch.date)}</time>
+    <span>{ch.country.flag} {ch.country.name}</span>
+  </p>
+  <h3>{ch.title}</h3>
+  <ScoreChange score={ch.score} />
+</a>

src/components/Changes.astro +16 −0
@@ -0,0 +1,16 @@
+---
+// The three most recent changes that moved a country's score (see /changes.yaml).
+import { changes } from "../lib/countries.js";
+import ChangeCard from "./ChangeCard.astro";
+
+const base = import.meta.env.BASE_URL;
+---
+<section class="changes" aria-labelledby="changesTitle">
+  <div class="changes-head">
+    <h2 class="changes-title" id="changesTitle">Latest changes</h2>
+    <a href={`${base}changes/`}>All changes →</a>
+  </div>
+  <div class="changes-grid">
+    {changes.slice(0, 3).map((ch) => <ChangeCard ch={ch} />)}
+  </div>
+</section>

src/components/ScoreChange.astro +13 −0
@@ -0,0 +1,13 @@
+---
+// Overall index score before → after, with the difference in points.
+import { tierColor } from "../lib/tiers.js";
+
+const { score } = Astro.props;
+const delta = score.after - score.before;
+---
+<p class="change-score" aria-label={`Score changed from ${score.before} to ${score.after}`}>
+  <span class="pill" style={`--c:${tierColor(score.before)}`}>{score.before}</span>
+  <span class="arrow" aria-hidden="true">→</span>
+  <span class="pill" style={`--c:${tierColor(score.after)}`}>{score.after}</span>
+  <span class={`delta ${delta >= 0 ? "up" : "down"}`}>{delta >= 0 ? "+" : "−"}{Math.abs(delta)} points</span>
+</p>

src/content.config.ts +17 −1
@@ -91,3 +91,19 @@ const segments = defineCollection({
 });
 
-export const collections = { countries, units, segments };
+// changes.yaml — developments that moved a country's score
+const changes = defineCollection({
+  loader: file("changes.yaml"),
+  schema: z
+    .object({
+      id: z.string(),
+      date: z.string().regex(/^\d{4}-\d{2}(-\d{2})?$/, "use YYYY-MM-DD or YYYY-MM"),
+      country: z.string().regex(/^[a-z]{2}$/), // file name in /countries
+      title: z.string().min(1),
+      description: z.string().min(1),
+      before: perSegment(score), // segment scores before the change
+      source,
+    })
+    .strict(),
+});
+
+export const collections = { countries, units, segments, changes };

src/layouts/Base.astro +1 −0
@@ -41,4 +41,5 @@ const fullTitle = title ? `${title} — Cannabis Freedom Index` : "Cannabis Free
       <a href={`${base}#methodology`}>Methodology</a>
       <a href={`${base}#story`}>Story</a>
+      <a href={`${base}changes/`}>Changes</a>
       <a href={`${base}compare/`}>Compare</a>
     </nav>

src/lib/countries.js +24 −1
@@ -105,7 +105,30 @@ export function subunitsOf(id) {
 }
 
+// ── Changes: developments that moved a score, newest first ──
+export const changes = (await getCollection("changes"))
+  .map(({ data }) => {
+    const country = countries.find((c) => c.slug === data.country);
+    if (!country) throw new Error(`changes.yaml: "${data.id}" refers to unknown country "${data.country}"`);
+    const s = scoreList(data.before);
+    const before = Math.round(scoreOf(s));
+    return {
+      ...data,
+      country,
+      source: pair(data.source),
+      score: { before, after: country.score },
+      // every segment, with its score before and after
+      segments: SEGMENTS.map((seg, i) => ({ name: seg.name, weight: seg.weight, before: s[i], after: country.s[i] })),
+    };
+  })
+  .sort((a, b) => b.date.localeCompare(a.date));
+for (const ch of changes) {
+  if (changes.filter((x) => x.country === ch.country).length > 1) throw new Error(`changes.yaml: more than one change for "${ch.country.slug}" — keep only the latest`);
+  if (ch.segments.every((seg) => seg.before === seg.after)) throw new Error(`changes.yaml: "${ch.id}" has the same scores before and after`);
+}
+
 // Review dates are stored as YYYY-MM-DD; this is how they are shown everywhere.
+// A date without a day ("2026-02") is shown as month and year.
 export const formatDate = (iso) =>
-  new Date(`${iso}T00:00:00Z`).toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric", timeZone: "UTC" });
+  new Date(`${iso.length === 7 ? iso + "-01" : iso}T00:00:00Z`).toLocaleDateString("en-GB", { day: iso.length === 7 ? undefined : "numeric", month: "long", year: "numeric", timeZone: "UTC" });
 
 // The most recent review date across every country and state file.

src/pages/changes/[id].astro +66 −0
@@ -0,0 +1,66 @@
+---
+import Base from "../../layouts/Base.astro";
+import ScoreChange from "../../components/ScoreChange.astro";
+import { changes, formatDate } from "../../lib/countries.js";
+import { tierColor } from "../../lib/tiers.js";
+
+export function getStaticPaths() {
+  return changes.map((ch, i) => ({ params: { id: ch.id }, props: { ch, newer: changes[i - 1], older: changes[i + 1] } }));
+}
+
+const { ch, newer, older } = Astro.props;
+const base = import.meta.env.BASE_URL;
+const c = ch.country;
+const diff = (seg) => seg.after - seg.before;
+---
+<Base title={ch.title} description={`${ch.title}: ${c.name} moved from ${ch.score.before} to ${ch.score.after} on the Cannabis Freedom Index. ${ch.description}`}>
+  <main class="change-page">
+    <div class="page-bar">
+      <a class="back" href={`${base}changes/`}>← All changes</a>
+      <a class="btn" href={`${base}country/${c.slug}/`}>{c.flag} {c.name} in full</a>
+    </div>
+
+    <article class="change-detail">
+      <p class="eyebrow"><time datetime={ch.date}>{formatDate(ch.date)}</time> · {c.name}</p>
+      <h1>{ch.title}</h1>
+      <p class="lede">{ch.description}</p>
+
+      <h2 class="d-h">Index score</h2>
+      <ScoreChange score={ch.score} />
+
+      <h2 class="d-h">Score by segment</h2>
+      <table class="change-table">
+        <thead>
+          <tr><th scope="col">Segment</th><th scope="col">Before</th><th scope="col">After</th><th scope="col">Change</th></tr>
+        </thead>
+        <tbody>
+          {ch.segments.map((seg) => (
+            <tr class={diff(seg) === 0 ? "same" : ""}>
+              <th scope="row">{seg.name}<small>{seg.weight}% of the index</small></th>
+              <td><span class="dot" style={`background:${tierColor(seg.before * 10)}`}></span>{seg.before}</td>
+              <td><span class="dot" style={`background:${tierColor(seg.after * 10)}`}></span>{seg.after}</td>
+              <td>{diff(seg) === 0
+                ? "—"
+                : <span class={`delta ${diff(seg) > 0 ? "up" : "down"}`}>{diff(seg) > 0 ? "+" : "−"}{Math.abs(diff(seg))}</span>}</td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+      <p class="change-note">Segments are scored from 0 to 10. The scores after the change are {c.name}'s current scores.</p>
+
+      <h2 class="d-h">Source</h2>
+      <ul class="srcs">
+        <li><a href={ch.source[1]} target="_blank" rel="noopener noreferrer">{ch.source[0]}</a></li>
+      </ul>
+    </article>
+
+    <nav class="pager" aria-label="Other changes">
+      {newer
+        ? <a href={`${base}changes/${newer.id}/`}><small>Newer · {formatDate(newer.date)}</small><span>{newer.country.flag} {newer.title}</span></a>
+        : <span></span>}
+      {older
+        ? <a class="next" href={`${base}changes/${older.id}/`}><small>Older · {formatDate(older.date)}</small><span>{older.country.flag} {older.title}</span></a>
+        : <span></span>}
+    </nav>
+  </main>
+</Base>

src/pages/changes/index.astro +43 −0
@@ -0,0 +1,43 @@
+---
+import Base from "../../layouts/Base.astro";
+import ScoreChange from "../../components/ScoreChange.astro";
+import { changes, formatDate } from "../../lib/countries.js";
+import { tierColor } from "../../lib/tiers.js";
+
+const base = import.meta.env.BASE_URL;
+// Newest first, grouped by year.
+const years = [...new Set(changes.map((ch) => ch.date.slice(0, 4)))]
+  .map((year) => ({ year, items: changes.filter((ch) => ch.date.startsWith(year)) }));
+---
+<Base title="Changes" description="New laws and other developments that moved a country's score on the Cannabis Freedom Index, newest first.">
+  <main class="changes-page">
+    <section class="hero hero-sm">
+      <p class="eyebrow">Changes</p>
+      <h1>What moved the scores</h1>
+      <p class="lede">New laws and other important developments that changed a country's score, newest first. Open one to see which segments moved and the source.</p>
+    </section>
+
+    <div class="chlog">
+      {years.map(({ year, items }) => (
+        <section aria-labelledby={`y${year}`}>
+          <h2 class="chl-year" id={`y${year}`}>{year}</h2>
+          <ol class="chl-list">
+            {items.map((ch) => (
+              <li class="chl-item" style={`--c:${tierColor(ch.score.after)}`}>
+                <a class="card chl-card" href={`${base}changes/${ch.id}/`}>
+                  <p class="change-meta">
+                    <time datetime={ch.date}>{formatDate(ch.date)}</time>
+                    <span>{ch.country.flag} {ch.country.name}</span>
+                  </p>
+                  <h3>{ch.title}</h3>
+                  <p class="chl-text">{ch.description}</p>
+                  <ScoreChange score={ch.score} />
+                </a>
+              </li>
+            ))}
+          </ol>
+        </section>
+      ))}
+    </div>
+  </main>
+</Base>

src/pages/index.astro +2 −0
@@ -3,4 +3,5 @@ import Base from "../layouts/Base.astro";
 import DidYouKnow from "../components/DidYouKnow.astro";
 import Lore from "../components/Lore.astro";
+import Changes from "../components/Changes.astro";
 import { SEGMENTS, DATA_SNAPSHOT, REC_LABEL, MED_LABEL, countries, regions, worldAvg, lastUpdated, formatDate } from "../lib/countries.js";
 import { TIERS, tierColor } from "../lib/tiers.js";
@@ -33,4 +34,5 @@ const mapData = countries.map(({ id, num, name, flag, raw, score, rank, s }) =>
       </div>
       <DidYouKnow />
+      <Changes />
     </section>
 

src/styles/global.css +53 −1
@@ -69,5 +69,7 @@ code { background: var(--surface-2); padding: 1px 6px; border-radius: 5px; font-
   border-radius: 10px; background: var(--accent); color: var(--accent-ink);
 }
-.nav { display: flex; gap: 22px; margin-left: auto; }
+.nav { display: flex; gap: clamp(12px, 2vw, 22px); margin-left: auto; }
+@media (min-width: 900px) { .brand { white-space: nowrap; } }
+@media (max-width: 899px) { .nav { gap: 14px; font-size: 14.5px; } }
 .nav a { color: var(--text-2); font-weight: 500; }
 .nav a:hover { color: var(--text); }
@@ -430,2 +432,52 @@ td.num, th.num { width: 52px; color: var(--muted); }
 .c-updated { margin: 28px 0 0; padding-top: 14px; border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; }
 .table-updated { margin: 12px 4px 0; color: var(--muted); font-size: 13px; text-align: right; }
+
+/* ── Changes ────────────────────────────────────────── */
+.changes { margin-top: 28px; }
+.changes-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
+.changes-title { font-size: 20px; margin: 0; }
+.changes-head a { color: var(--text-2); font-size: 14px; font-weight: 500; white-space: nowrap; }
+.changes-head a:hover { color: var(--text); }
+.changes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
+.change { padding: 18px 20px; display: flex; flex-direction: column; color: inherit; transition: border-color .15s, transform .15s; }
+.change:hover { border-color: var(--muted); transform: translateY(-1px); }
+.change-meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; margin: 0 0 8px; color: var(--muted); font-size: 12.5px; }
+.change-meta span { color: var(--text-2); font-weight: 500; }
+.change h3 { font-size: 17px; line-height: 1.25; margin: 0 0 14px; flex: 1; }
+.change-score { display: flex; align-items: center; gap: 8px; margin: 0; font-variant-numeric: tabular-nums; }
+.pill { min-width: 38px; padding: 3px 9px; border-radius: 999px; text-align: center; font-weight: 700; font-size: 15px; color: var(--text); border: 2px solid var(--c); background: color-mix(in srgb, var(--c) 18%, transparent); }
+.change-score .arrow { color: var(--muted); }
+.delta { font-size: 13px; font-weight: 600; }
+.change-score .delta { margin-left: 4px; }
+.delta.up { color: var(--t5); }
+.delta.down { color: var(--t1); }
+
+.changes-page { padding-bottom: 20px; }
+.chlog { max-width: 760px; margin-top: 28px; }
+.chl-year { font-size: 22px; margin: 0 0 14px; }
+.chl-list { list-style: none; margin: 0 0 32px; padding: 0 0 0 28px; position: relative; display: grid; gap: 14px; }
+.chl-list::before { content: ""; position: absolute; left: 6px; top: 6px; bottom: 6px; width: 2px; border-radius: 1px; background: var(--border); }
+.chl-item { position: relative; }
+.chl-item::before { content: ""; position: absolute; left: -28px; top: 20px; width: 10px; height: 10px; border-radius: 50%; background: var(--c); border: 2px solid var(--bg); box-shadow: 0 0 0 2px var(--border); }
+.chl-card { display: block; padding: 16px 20px; color: inherit; transition: border-color .15s; }
+.chl-card:hover { border-color: var(--muted); }
+.chl-card h3 { font-size: 18px; line-height: 1.25; margin: 0 0 6px; }
+.chl-text { color: var(--text-2); font-size: 14.5px; margin: 0 0 12px; }
+.change-page { padding-top: 26px; padding-bottom: 20px; }
+.change-detail { max-width: 760px; }
+.change-detail h1 { font-size: clamp(30px, 4.4vw, 46px); line-height: 1.1; margin: 8px 0 16px; }
+.change-detail .change-score .pill { font-size: 20px; min-width: 52px; padding: 5px 12px; }
+.change-detail .change-score .delta { font-size: 15px; }
+.change-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
+.change-table th, .change-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); text-align: left; vertical-align: middle; }
+.change-table thead th { color: var(--muted); font-size: 12.5px; font-weight: 500; }
+.change-table th { cursor: default; white-space: normal; }
+.change-table tbody th { font-weight: 600; font-size: 15px; color: var(--text); text-transform: none; letter-spacing: 0; }
+.change-table tbody th small { text-transform: none; letter-spacing: 0; }
+.change-table tbody tr:last-child :is(th, td) { border-bottom: 0; }
+.change-table tbody th small { display: block; color: var(--muted); font-weight: 400; font-size: 12.5px; }
+.change-table td { font-weight: 600; white-space: nowrap; }
+.change-table tr.same :is(th, td) { color: var(--muted); font-weight: 400; }
+.change-table .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; }
+.change-table .delta { font-size: inherit; }
+.change-note { color: var(--muted); font-size: 13px; margin: 10px 0 0; }

    ↑↓ to move, ↵ to open.