Cannabis Freedom Index

Changelog 937e350

rotating cannabis altnames on homapge

3 files of code, design or docs +60 −15 lines

Code, design and documents 3 files

source-archive.json +13 −13

Not shown: a picture, a generated file, or past the length shown per change.

src/components/HomePage.astro +35 −2
@@ -14,5 +14,5 @@ import { SHOW_PRICES } from "../lib/config.js";
 import { homeSeo } from "../lib/seo.js";
 import { viewByMetric } from "../lib/map-views.js";
-import { cultureOf } from "../lib/culture.js";
+import { cultureOf, cultureEntries } from "../lib/culture.js";
 import { panelRows, PANEL_FROM } from "../lib/exports.js";
 
@@ -31,4 +31,13 @@ const { view = null } = Astro.props;
 
 const base = import.meta.env.BASE_URL;
+// The hero's "cannabis" cycles through other names for the plant from the atlas, each with the
+// language it comes from and a link to its entry. Only names for the plant itself, not for a
+// joint, a strain or a deal.
+const HERO_WORDS = ["cannabis", "ganja", "marijuana", "dagga", "kif", "maconha", "esrar", "mota", "anasha", "chamba", "wiet", "zatla", "pakalolo", "mbanje", "hashish", "matekoane", "zamal", "bangi", "diamba", "charas", "bhang", "grass", "pot", "dope"];
+const heroWords = HERO_WORDS.map((slug) => {
+  const e = cultureEntries.find((x) => x.url === `culture/language/${slug}/`);
+  if (!e) throw new Error(`HomePage: no atlas entry "language/${slug}" for the hero`);
+  return { word: e.title.toLowerCase(), note: e.origin ? e.origin.split(",")[0] : e.countries[0].name, href: `${base}${e.url}` };
+});
 // where each map option lives: its own page, or the home page for the overall index
 const viewHref = (metric) => `${base}map/${viewByMetric[metric].slug ? viewByMetric[metric].slug + "/" : ""}`;
@@ -165,5 +174,5 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
       )}
       <p class="eyebrow">Cannabis Freedom Index <span>{DATA_SNAPSHOT} edition</span></p>
-      <h1>How free is cannabis <em>in every country?</em></h1>
+      <h1>How free is <span class="visually-hidden">cannabis</span><a class="hero-word" id="heroWord" href={heroWords[0].href} aria-hidden="true" tabindex="-1" data-words={JSON.stringify(heroWords)}><span class="hw-w">{heroWords[0].word}</span><small class="hw-note">{heroWords[0].note}</small></a> <em>in every country?</em></h1>
       <div class="front-grid">
         <div class="front-intro">
@@ -943,2 +952,26 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments",
   </script>
 </Base>
+
+<script>
+  // The hero's word for cannabis: a new one every few seconds, held while the pointer is on it,
+  // and still for anyone who prefers less motion.
+  const hw = document.getElementById("heroWord");
+  if (hw && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
+    const words = JSON.parse(hw.dataset.words);
+    const word = hw.querySelector(".hw-w"), note = hw.querySelector(".hw-note");
+    let i = 0, held = false;
+    hw.addEventListener("mouseenter", () => (held = true));
+    hw.addEventListener("mouseleave", () => (held = false));
+    setInterval(() => {
+      if (held || document.hidden) return;
+      i = (i + 1) % words.length;
+      hw.classList.add("out");
+      setTimeout(() => {
+        word.textContent = words[i].word;
+        note.textContent = words[i].note;
+        hw.href = words[i].href;
+        hw.classList.remove("out");
+      }, 380);
+    }, 2800);
+  }
+</script>

src/styles/global.css +12 −0
@@ -2037,2 +2037,14 @@ button.sv-row .sv-chip { font: 400 10.5px var(--mono); text-transform: uppercase
 .legend-poster { margin-left: auto; color: var(--text-2); text-decoration: underline; text-decoration-color: var(--muted); text-underline-offset: 3px; }
 .legend-poster:hover { color: var(--text); }
+
+/* The hero's word for cannabis, cycling through the atlas's names for it (HomePage.astro), with
+   the language each comes from set small beside it. The question's second half keeps its line. */
+.front h1 em { display: block; }
+.hero-word { position: relative; display: inline-block; color: inherit; }
+.hw-w { display: inline-block; transition: opacity .38s ease, transform .38s ease, filter .38s ease; }
+.hero-word.out .hw-w { opacity: 0; transform: translateY(.14em); filter: blur(6px); }
+.hero-word:hover .hw-w { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: .1em; text-decoration-color: var(--muted); }
+.hw-note { position: absolute; left: 100%; top: .32em; margin-left: .45em; font: 400 11.5px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); white-space: nowrap; transition: opacity .38s ease; }
+.hero-word.out .hw-note { opacity: 0; }
+@media (max-width: 700px) { .hw-note { left: auto; right: 0; top: auto; bottom: 100%; margin: 0 0 2px; font-size: 10px; } }
+@media (prefers-reduced-motion: reduce) { .hw-w, .hw-note { transition: none; } }

    ↑↓ to move, ↵ to open.