<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://asist.a2hosted.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Hthach</id>
	<title>Biographical Directory of Documentation and Information - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://asist.a2hosted.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Hthach"/>
	<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php/Special:Contributions/Hthach"/>
	<updated>2026-06-05T13:07:45Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=Samantha_Hastings&amp;diff=2686</id>
		<title>Samantha Hastings</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=Samantha_Hastings&amp;diff=2686"/>
		<updated>2026-05-18T15:52:48Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Created page with &amp;quot;This is a stub. Contact us if you&amp;#039;d like to author the page.&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This is a stub. Contact us if you&#039;d like to author the page.&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2685</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2685"/>
		<updated>2026-05-18T15:52:01Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2571</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2571"/>
		<updated>2026-03-29T13:25:52Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function personName(person) {&lt;br /&gt;
    return ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function toYear(value) {&lt;br /&gt;
  if (!value) return &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  var s = String(value).trim();&lt;br /&gt;
&lt;br /&gt;
  // First: look for a 4-digit year anywhere in the string&lt;br /&gt;
  var m4 = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
  if (m4) return m4[1];&lt;br /&gt;
&lt;br /&gt;
  // Next: handle common numeric dates like 4/7/11 or 4-7-11&lt;br /&gt;
  var m2 = s.match(/^\d{1,2}[\/-]\d{1,2}[\/-](\d{2})$/);&lt;br /&gt;
  if (m2) {&lt;br /&gt;
    var yy = Number(m2[1]);&lt;br /&gt;
&lt;br /&gt;
    // Adjust pivot if needed&lt;br /&gt;
    return String(yy &amp;lt;= 29 ? 2000 + yy : 1900 + yy);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return &#039;&#039;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== CSV PARSER ======&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var meta = lifeStr ? &#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039; : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(name, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + name + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
&lt;br /&gt;
    var groups = [&lt;br /&gt;
      { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
      { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
      { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
    ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!groups.length) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;Information unavailable. Please contact us if you have details about this record.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else if (groups.length === 1) {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          groups.map(function (g) {&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                  buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
                &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
            );&lt;br /&gt;
          }).join(&#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i]);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio ohr-m-0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.remove(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.add(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-mt-sm&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2570</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2570"/>
		<updated>2026-03-29T13:24:46Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function personName(person) {&lt;br /&gt;
    return ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function toYear(value) {&lt;br /&gt;
  if (!value) return &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  var s = String(value).trim();&lt;br /&gt;
&lt;br /&gt;
  // First: look for a 4-digit year anywhere in the string&lt;br /&gt;
  var m4 = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
  if (m4) return m4[1];&lt;br /&gt;
&lt;br /&gt;
  // Next: handle common numeric dates like 4/7/11 or 4-7-11&lt;br /&gt;
  var m2 = s.match(/^\d{1,2}[\/-]\d{1,2}[\/-](\d{2})$/);&lt;br /&gt;
  if (m2) {&lt;br /&gt;
    var yy = Number(m2[1]);&lt;br /&gt;
&lt;br /&gt;
    // Adjust pivot if needed&lt;br /&gt;
    return String(yy &amp;lt;= 29 ? 2000 + yy : 1900 + yy);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return &#039;&#039;;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== CSV PARSER ======&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var meta = lifeStr ? &#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039; : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(name, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + name + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
&lt;br /&gt;
    var groups = [&lt;br /&gt;
      { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
      { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
      { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
    ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!groups.length) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;Information unavailable. Please contact us if you have details about this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else if (groups.length === 1) {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          groups.map(function (g) {&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                  buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
                &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
            );&lt;br /&gt;
          }).join(&#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i]);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio ohr-m-0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.remove(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.add(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-mt-sm&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2569</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2569"/>
		<updated>2026-03-28T21:55:25Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=Arthur_Jack_Meadows&amp;diff=2568</id>
		<title>Arthur Jack Meadows</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=Arthur_Jack_Meadows&amp;diff=2568"/>
		<updated>2026-03-28T20:32:17Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;__NOINDEX__&lt;br /&gt;
[[File:Jack-meadows.jpg|alt=Arthur Jack Meadows|thumb|&amp;lt;nowiki&amp;gt;Arthur Jack Meadows | Credit &amp;lt;/nowiki&amp;gt;[https://www.asist.org/2016/08/18/jack-meadows-1934-2016/ ASIS&amp;amp;T]]]&lt;br /&gt;
NOTE: This is a stub.&lt;br /&gt;
&lt;br /&gt;
Arthur Jack Meadows (1934 - 2016) was a British astronomer and information scientist whose career spanned astronomy, history of science, and information studies. He founded the astronomy department at the University of Leicester and later advanced information science at Loughborough University. A prolific scholar, he authored over 30 books and 250 articles, and played key roles in professional organizations. His work reflected a lifelong commitment to understanding and improving the communication of knowledge.&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2567</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2567"/>
		<updated>2026-03-28T19:42:56Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function personName(person) {&lt;br /&gt;
    return ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== CSV PARSER ======&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var meta = lifeStr ? &#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039; : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(name, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + name + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session) {&lt;br /&gt;
    var name = personName(person);&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
&lt;br /&gt;
    var groups = [&lt;br /&gt;
      { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
      { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
      { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
    ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!groups.length) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;Information unavailable. Please contact us if you have details about this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else if (groups.length === 1) {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          groups.map(function (g) {&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                  buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
                &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
            );&lt;br /&gt;
          }).join(&#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i]);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio ohr-m-0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.remove(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.add(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted ohr-mt-sm&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2566</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2566"/>
		<updated>2026-03-28T19:40:33Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px;&lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
.ohr-center { text-align: center; }&lt;br /&gt;
.ohr-mt-sm { margin-top: 0.5rem; }&lt;br /&gt;
.ohr-m-0 { margin: 0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot {&lt;br /&gt;
  margin: 0 0.25rem;&lt;br /&gt;
  color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.25rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session {&lt;br /&gt;
  margin-top: 0.5rem;&lt;br /&gt;
  padding-top: 0.5rem;&lt;br /&gt;
  border-top: 1px solid #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Indented content under the session title */&lt;br /&gt;
.ohr-session &amp;gt; *:not(.ohr-session__title) {&lt;br /&gt;
  margin-left: 0.6rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800;&lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover {&lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  border-top: none;&lt;br /&gt;
  padding-top: 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=List_of_Oral_Histories&amp;diff=2565</id>
		<title>List of Oral Histories</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=List_of_Oral_Histories&amp;diff=2565"/>
		<updated>2026-03-28T19:36:34Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Created page with &amp;quot;&amp;lt;!-- ===== Oral History Records (HTML) ===== --&amp;gt; &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;   &amp;lt;div id=&amp;quot;ohr-loading&amp;quot; class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center; padding: 1rem;&amp;quot;&amp;gt;     Loading oral history records…   &amp;lt;/div&amp;gt;    &amp;lt;div id=&amp;quot;ohr-error&amp;quot; class=&amp;quot;ohr-hidden&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;    &amp;lt;div id=&amp;quot;ohr-list&amp;quot; class=&amp;quot;ohr-list&amp;quot;&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt;&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;!-- ===== Oral History Records (HTML) ===== --&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div id=&amp;quot;ohr-loading&amp;quot; class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center; padding: 1rem;&amp;quot;&amp;gt;&lt;br /&gt;
    Loading oral history records…&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div id=&amp;quot;ohr-error&amp;quot; class=&amp;quot;ohr-hidden&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div id=&amp;quot;ohr-list&amp;quot; class=&amp;quot;ohr-list&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2564</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2564"/>
		<updated>2026-03-28T19:34:35Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Sort sessions inside each person by Date From/To year (descending)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function buildSessionHTML(person, session, idx) {&lt;br /&gt;
  var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  var title = session.recorded; // No more &amp;quot;(Interview #)&amp;quot; suffix&lt;br /&gt;
&lt;br /&gt;
  // Determine which resource types exist for this session&lt;br /&gt;
  var groups = [&lt;br /&gt;
    { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
    { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
    { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
  ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
  var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  if (!groups.length) {&lt;br /&gt;
    links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;Record details are currently unknown. Please contact us if you can provide additional information.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
  } else if (groups.length === 1) {&lt;br /&gt;
    // Single type → no subheading needed; keep on one line (wrap as needed)&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  } else {&lt;br /&gt;
    // Multiple types → labeled groups on separate lines&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        groups.map(function (g) {&lt;br /&gt;
          return (&lt;br /&gt;
            &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
            &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }).join(&#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      links +&lt;br /&gt;
    &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
  btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
    var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (open) {&lt;br /&gt;
      panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
      div.classList.remove(&#039;is-open&#039;);          // ✅ closed state&lt;br /&gt;
      btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
      btn.textContent = &#039;Details&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      div.classList.add(&#039;is-open&#039;);             // ✅ open state&lt;br /&gt;
      btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
      btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2560</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2560"/>
		<updated>2026-02-22T21:55:53Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9; &lt;br /&gt;
  border-color: #94a3b8; /* Darkened border for definition */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darkened for better accessibility (WCAG AA) */&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #cbd5e1; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4; /* Slightly more air for readability */&lt;br /&gt;
  color: #374151; /* Darker for better contrast */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.25rem; /* Reduced from 0.35rem to sit closer to bio */&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.5rem;&lt;br /&gt;
  padding-top: 0.5rem;&lt;br /&gt;
  border-top: 1px solid #cbd5e1; /* Sessions now own the border separating them from the Wiki button section */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Indented content under the session title (labels and buttons) */&lt;br /&gt;
.ohr-session &amp;gt; *:not(.ohr-session__title) {&lt;br /&gt;
  margin-left: 0.6rem; /* Ensures indentation even if headings are hidden */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; &lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff; &lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Divider now only handles vertical spacing to avoid duplicate lines */&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.2rem; /* Reduced from 0.4rem */&lt;br /&gt;
  border-top: none; &lt;br /&gt;
  padding-top: 0.2rem; /* Reduced from 0.4rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2559</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2559"/>
		<updated>2026-02-22T21:54:26Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9; &lt;br /&gt;
  border-color: #94a3b8; /* Darkened border for definition */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darkened for better accessibility (WCAG AA) */&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #cbd5e1; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4; /* Slightly more air for readability */&lt;br /&gt;
  color: #374151; /* Darker for better contrast */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.5rem;&lt;br /&gt;
  padding-top: 0.5rem;&lt;br /&gt;
  border-top: 1px solid #cbd5e1; /* Sessions now own the border separating them from the Wiki button section */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Indented content under the session title (labels and buttons) */&lt;br /&gt;
.ohr-session &amp;gt; *:not(.ohr-session__title) {&lt;br /&gt;
  margin-left: 0.6rem; /* Ensures indentation even if headings are hidden */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; &lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff; &lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Divider now only handles vertical spacing to avoid duplicate lines */&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: none; &lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2558</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2558"/>
		<updated>2026-02-22T21:52:09Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9; &lt;br /&gt;
  border-color: #94a3b8; /* Darkened border for definition */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darkened for better accessibility (WCAG AA) */&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #cbd5e1; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4; /* Slightly more air for readability */&lt;br /&gt;
  color: #374151; /* Darker for better contrast */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0; &lt;br /&gt;
  border-left: none; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Indented labels and buttons under the session title */&lt;br /&gt;
.ohr-resource-label,&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  padding-left: 0.5rem; /* Indentation for alignment hierarchy */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; &lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff; &lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: 1px solid #cbd5e1; /* Restored border between Wiki button and Sessions */&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2557</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2557"/>
		<updated>2026-02-22T21:50:26Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9; &lt;br /&gt;
  border-color: #94a3b8; /* Darkened border for definition */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darkened for better accessibility (WCAG AA) */&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #cbd5e1; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4; /* Slightly more air for readability */&lt;br /&gt;
  color: #374151; /* Darker for better contrast */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0; &lt;br /&gt;
  border-left: none; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; &lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff; &lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px); &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: none; /* Removed the border as requested */&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2556</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2556"/>
		<updated>2026-02-22T21:48:28Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
  font-family: inherit;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state - Enhanced contrast */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9; &lt;br /&gt;
  border-color: #94a3b8; /* Darkened border for definition */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darkened for better accessibility (WCAG AA) */&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #cbd5e1; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.4; /* Slightly more air for readability */&lt;br /&gt;
  color: #374151; /* Darker for better contrast */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #cbd5e1; /* Darker indicator */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; /* Extra bold for visibility */&lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; /* High contrast charcoal */&lt;br /&gt;
  margin: 0.2rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: #fff; /* Solid white background for the button */&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #94a3b8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:active {&lt;br /&gt;
  transform: translateY(1px); /* Slight click feedback */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: 1px solid #cbd5e1;&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2555</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2555"/>
		<updated>2026-02-22T21:45:58Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Sort sessions inside each person by Date From/To year (descending)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function buildSessionHTML(person, session, idx) {&lt;br /&gt;
  var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  var title = session.recorded; // No more &amp;quot;(Interview #)&amp;quot; suffix&lt;br /&gt;
&lt;br /&gt;
  // Determine which resource types exist for this session&lt;br /&gt;
  var groups = [&lt;br /&gt;
    { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
    { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
    { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
  ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
  var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  if (!groups.length) {&lt;br /&gt;
    links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
  } else if (groups.length === 1) {&lt;br /&gt;
    // Single type → no subheading needed; keep on one line (wrap as needed)&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  } else {&lt;br /&gt;
    // Multiple types → labeled groups on separate lines&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        groups.map(function (g) {&lt;br /&gt;
          return (&lt;br /&gt;
            &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
            &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }).join(&#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      links +&lt;br /&gt;
    &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
  btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
    var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (open) {&lt;br /&gt;
      panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
      div.classList.remove(&#039;is-open&#039;);          // ✅ closed state&lt;br /&gt;
      btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
      btn.textContent = &#039;Details&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      div.classList.add(&#039;is-open&#039;);             // ✅ open state&lt;br /&gt;
      btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
      btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2554</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2554"/>
		<updated>2026-02-22T21:45:20Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Sort sessions inside each person by Date From/To year (descending)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function buildSessionHTML(person, session, idx) {&lt;br /&gt;
  var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  var title = session.recorded; // No more &amp;quot;(Interview #)&amp;quot; suffix&lt;br /&gt;
&lt;br /&gt;
  // Determine which resource types exist for this session&lt;br /&gt;
  var groups = [&lt;br /&gt;
    { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
    { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
    { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
  ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
  var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  if (!groups.length) {&lt;br /&gt;
    links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
  } else if (groups.length === 1) {&lt;br /&gt;
    // Single type → no subheading needed; keep on one line (wrap as needed)&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  } else {&lt;br /&gt;
    // Multiple types → labeled groups on separate lines&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        groups.map(function (g) {&lt;br /&gt;
          return (&lt;br /&gt;
            &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
            &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }).join(&#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      links +&lt;br /&gt;
    &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2553</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2553"/>
		<updated>2026-02-22T21:43:52Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  var PEOPLE = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim().replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(person.first + &#039; &#039; + person.last) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        person.content +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var isOpen = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
        if (isOpen) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.remove(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.add(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    if (!root) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2552</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2552"/>
		<updated>2026-02-22T21:42:00Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  var PEOPLE = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    return (s || &#039;&#039;).replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;).toString().toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    return (value.match(re) || []);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(person.name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        person.content +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var isOpen = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
&lt;br /&gt;
        if (isOpen) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.remove(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          div.classList.add(&#039;is-open&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      buildToolbar($(&#039;ohr-directory&#039;));&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2551</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2551"/>
		<updated>2026-02-22T21:41:46Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; &lt;br /&gt;
  flex-wrap: wrap; &lt;br /&gt;
  gap: 0.5rem; &lt;br /&gt;
  align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { &lt;br /&gt;
  margin-left: auto; &lt;br /&gt;
  font-size: 0.82rem; &lt;br /&gt;
  color: #6b7280; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Open (expanded) state */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #f1f5f9;        /* slate tint */&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  box-shadow: 0 2px 4px rgba(0,0,0,0.03);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-meta .ohr-dot { &lt;br /&gt;
  margin: 0 0.25rem; &lt;br /&gt;
  color: #e2e8f0; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Bio --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible,&lt;br /&gt;
.ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #edf2f7;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #57606a;&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #edf2f7;&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2550</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2550"/>
		<updated>2026-02-22T21:38:40Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Darker blue background for the open state */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #eef6ff; &lt;br /&gt;
  border-color: #cbdcf0;&lt;br /&gt;
  box-shadow: 0 2px 4px rgba(0,0,0,0.03);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem; /* Slight gap for breathing room */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096; /* Darkened slightly for better visibility */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #edf2f7;&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #57606a; /* Darkened to meet WCAG contrast ratios */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568; /* Darkened for better contrast */&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #edf2f7;&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2549</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2549"/>
		<updated>2026-02-22T21:38:18Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Darker blue background for the open state.&lt;br /&gt;
   Applying !important to the container ensures the highlight is visible.&lt;br /&gt;
*/&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #dae9ff !important; &lt;br /&gt;
  border-color: #a5c7f9;&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1; /* Tight line-height to fix perceived padding issues */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.1rem; /* Precise small gap */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
  line-height: 1; /* Match name for symmetry */&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darker labels for contrast */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #d1d5db; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #a5c7f9; /* Match open state border color */&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels - High Contrast */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; /* Extra weight for visibility */&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; /* Charcoal for accessibility compliance */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: white; /* Solid bg for better definition */&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Accordion Panel Background */&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
  background: transparent; /* Ensures the panel matches the item&#039;s background */&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #a5c7f9; /* Match active border color */&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2548</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2548"/>
		<updated>2026-02-22T21:37:15Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Darker blue background for the open state.&lt;br /&gt;
   We use !important here to ensure that even if a hover state &lt;br /&gt;
   or specific script-injected style exists, the &amp;quot;Open&amp;quot; look persists.&lt;br /&gt;
*/&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #dae9ff !important; &lt;br /&gt;
  border-color: #a5c7f9;&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1; /* Tight line-height to fix perceived padding issues */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.1rem; /* Precise small gap */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
  line-height: 1; /* Match name for symmetry */&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darker labels for contrast */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #d1d5db; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #a5c7f9; /* Match open state border color */&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels - High Contrast */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; /* Extra weight for visibility */&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; /* Charcoal for accessibility compliance */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: white; /* Solid bg for better definition */&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #a5c7f9; /* Match active border color */&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2547</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2547"/>
		<updated>2026-02-22T21:35:13Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Darker blue background for the open state */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #eef6ff; &lt;br /&gt;
  border-color: #cbdcf0;&lt;br /&gt;
  box-shadow: 0 2px 4px rgba(0,0,0,0.03);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem; /* Slight gap for breathing room */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096; /* Darkened slightly for better visibility */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #edf2f7;&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #57606a; /* Darkened to meet WCAG contrast ratios */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568; /* Darkened for better contrast */&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #edf2f7;&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2546</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2546"/>
		<updated>2026-02-22T21:34:17Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 240px;&lt;br /&gt;
  padding: 0.4rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Enhanced Focus State */&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.85rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.75rem 1rem;&lt;br /&gt;
  box-shadow: 0 1px 3px rgba(0,0,0,0.05);&lt;br /&gt;
  margin-bottom: 0.6rem;&lt;br /&gt;
  border: 1px solid transparent;&lt;br /&gt;
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Container Hover Depth */&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  transform: translateY(-1px);&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);&lt;br /&gt;
  border-color: #e0e7ff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.05rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Colors */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.1rem;&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  color: #111; /* Value color */&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #6b7280; /* Softer label color */&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.3rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.4rem 0 0;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  line-height: 1.4;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.6rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.25rem 0.6rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.5rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #dce8f6;&lt;br /&gt;
  border: 1px solid #c8d9ef;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  border-color: #b0c9e8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Focus states for buttons */&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.35rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.2rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  margin: 0.15rem 0 0.25rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.25rem 0.5rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f3f4f6;&lt;br /&gt;
  border-color: #9ca3af;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.6rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.6rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.6rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.75rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2545</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2545"/>
		<updated>2026-02-22T21:33:27Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
/* These styles help integrate the list into a multi-column layout */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; /* Narrower container for better readability */&lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; /* Tighter gap between list and sidebar */&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem; /* Reduced from 0.75rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-radius: 0.4rem; /* Sharper corners for a cleaner look */&lt;br /&gt;
  padding: 0.4rem 0.75rem; /* Significantly reduced vertical padding */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.03);&lt;br /&gt;
  margin-bottom: 0.3rem; /* Tighter vertical rhythm in the list */&lt;br /&gt;
  border: 1px solid rgba(224, 231, 255, 0.4);&lt;br /&gt;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Container Hover Depth */&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  transform: translateY(-1px);&lt;br /&gt;
  box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.06);&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; /* Keep everything vertically centered for unexpanded state */&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.95rem; /* Slightly smaller for density */&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  line-height: 1.1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Colors */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #94a3b8; /* Lighter label color for better hierarchy */&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.01em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.25rem 0 0; /* Reduced margin */&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  line-height: 1.3;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  /* Option: uncomment below to limit bio length in list view */&lt;br /&gt;
  /* display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.3rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #eef4fb; /* Softer background */&lt;br /&gt;
  border: 1px solid #d9e6f5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e1ecf8;&lt;br /&gt;
  border-color: #c4d8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.25rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #a1a1aa;&lt;br /&gt;
  margin: 0.1rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e5e7eb;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #d1d5db;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.4rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.05);&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2544</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2544"/>
		<updated>2026-02-22T21:32:31Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  /* Aggressively balanced padding */&lt;br /&gt;
  padding: 0.5rem 0.75rem; &lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Clearer Blue Background for Open State */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #dae9ff !important; /* Forced darker blue background */&lt;br /&gt;
  border-color: #a5c7f9;&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1; /* Forced tight line-height to remove vertical &#039;lead&#039; */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin: 0.1rem 0 0; /* Minimal top margin for alignment */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
  line-height: 1; /* Match name for symmetry */&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #57606a; /* Darker labels for contrast */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #d1d5db; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.6rem 0 0.2rem; &lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #a5c7f9;&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* High Contrast Resource Labels (WCAG AA Compliant) */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 800; /* Bolder for legibility */&lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #374151; /* Darker charcoal for high contrast */&lt;br /&gt;
  margin: 0.25rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: white;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #374151; &lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: 1px solid #a5c7f9;&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2543</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2543"/>
		<updated>2026-02-22T21:30:42Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  /* Balanced padding: 0.5rem top for name, 0.5rem bottom for meta */&lt;br /&gt;
  padding: 0.5rem 0.75rem; &lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Significantly clearer blue background for the open state */&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #e1effe; /* Deeper, more distinct light blue */&lt;br /&gt;
  border-color: #b6d4fe; /* Defined border for the expanded card */&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem; &lt;br /&gt;
  margin-bottom: 0; /* Ensures the years sit precisely 0.5rem from the bottom edge */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #d1d5db; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; &lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #cbdcf0; /* Subtle blue line to match expanded state */&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels - Darkened for high contrast */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #4a5568; &lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568; &lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #cbdcf0; /* Divider matches expanded border */&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2542</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2542"/>
		<updated>2026-02-22T21:28:25Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item.is-open {&lt;br /&gt;
  background: #eef6ff; &lt;br /&gt;
  border-color: #cbdcf0;&lt;br /&gt;
  box-shadow: 0 2px 4px rgba(0,0,0,0.03);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem; /* Slight gap for breathing room */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096; /* Darkened slightly for better visibility */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #edf2f7;&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #57606a; /* Darkened to meet WCAG contrast ratios */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568; /* Darkened for better contrast */&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #edf2f7;&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2541</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2541"/>
		<updated>2026-02-22T21:25:12Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; &lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; &lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #fcfdfe;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem 0.75rem; /* Balanced padding (0.5rem top/bottom) */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.02);&lt;br /&gt;
  margin-bottom: 0.2rem;&lt;br /&gt;
  border: 1px solid #edf2f7;&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; &lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.92rem; &lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Hierarchy */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem; /* Slight gap for breathing room */&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  color: #4a5568;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #718096; /* Darkened slightly for better visibility */&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.5rem 0 0.2rem; /* Balanced top margin to match item top padding */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: all 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.25rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
  background: #edf2f7;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2e8f0;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Expanded State) --- */&lt;br /&gt;
.ohr-session { &lt;br /&gt;
  margin-top: 0.2rem;&lt;br /&gt;
  padding-left: 0.25rem;&lt;br /&gt;
  border-left: 2px solid #edf2f7;&lt;br /&gt;
}&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #1a202c;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* WCAG Compliant Resource Labels */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #57606a; /* Darkened to meet WCAG contrast ratios */&lt;br /&gt;
  margin: 0.15rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.3rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e2e8f0;&lt;br /&gt;
  border-radius: 0.25rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.72rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4a5568; /* Darkened for better contrast */&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f7fafc;&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
  color: #2d3748;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.35rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.35rem;&lt;br /&gt;
  border-top: 1px solid #edf2f7;&lt;br /&gt;
  padding-top: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #c53030;&lt;br /&gt;
  background: #fff5f5;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.5rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  border: 1px solid #feb2b2;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2540</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2540"/>
		<updated>2026-02-22T21:22:31Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
/* These styles help integrate the list into a multi-column layout */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1100px; /* Narrower container for better readability */&lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.25rem; /* Tighter gap between list and sidebar */&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.5rem; /* Reduced from 0.75rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.82rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f8fbff;&lt;br /&gt;
  border-radius: 0.4rem; /* Sharper corners for a cleaner look */&lt;br /&gt;
  padding: 0.4rem 0.75rem; /* Significantly reduced vertical padding */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.03);&lt;br /&gt;
  margin-bottom: 0.3rem; /* Tighter vertical rhythm in the list */&lt;br /&gt;
  border: 1px solid rgba(224, 231, 255, 0.4);&lt;br /&gt;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Container Hover Depth */&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  transform: translateY(-1px);&lt;br /&gt;
  box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.06);&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
  align-items: center; /* Keep everything vertically centered for unexpanded state */&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 0.95rem; /* Slightly smaller for density */&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  line-height: 1.1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Colors */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #94a3b8; /* Lighter label color for better hierarchy */&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  letter-spacing: 0.01em;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.25rem; color: #e2e8f0; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.25rem 0 0; /* Reduced margin */&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  line-height: 1.3;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  /* Option: uncomment below to limit bio length in list view */&lt;br /&gt;
  /* display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.15rem 0.45rem;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.3rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #eef4fb; /* Softer background */&lt;br /&gt;
  border: 1px solid #d9e6f5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e1ecf8;&lt;br /&gt;
  border-color: #c4d8f0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.25rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.1rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.65rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #a1a1aa;&lt;br /&gt;
  margin: 0.1rem 0 0.15rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e5e7eb;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.15rem 0.4rem;&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #d1d5db;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.4rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.4rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.05);&lt;br /&gt;
  padding-top: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.3rem;&lt;br /&gt;
  padding: 0.6rem;&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2539</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2539"/>
		<updated>2026-02-22T21:20:45Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Page Layout Adjustments --- */&lt;br /&gt;
/* These styles help integrate the list into a multi-column layout */&lt;br /&gt;
.ohr-container {&lt;br /&gt;
  max-width: 1200px;&lt;br /&gt;
  margin: 0 auto;&lt;br /&gt;
  padding: 0 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-layout-grid {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 1.5rem; /* Reduced gap between list and sidebar */&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-list-wrapper {&lt;br /&gt;
  flex: 1;&lt;br /&gt;
  min-width: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 200px;&lt;br /&gt;
  padding: 0.4rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.85rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f8fbff; /* Slightly lighter for better contrast with page bg */&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 0.6rem 0.9rem; /* Further reduced padding */&lt;br /&gt;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);&lt;br /&gt;
  margin-bottom: 0.4rem; /* Tight list spacing */&lt;br /&gt;
  border: 1px solid rgba(224, 231, 255, 0.5);&lt;br /&gt;
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Container Hover Depth */&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  transform: translateY(-1px);&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.07);&lt;br /&gt;
  border-color: #cbd5e1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1rem; /* Scaled down slightly more */&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Colors */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.05rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.3rem; color: #d1d5db; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.35rem 0 0;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  line-height: 1.35;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.5rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.2rem 0.5rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.4rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #e5effa;&lt;br /&gt;
  border: 1px solid #d1e2f6;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #dce8f6;&lt;br /&gt;
  border-color: #b0c9e8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.3rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.15rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.68rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.02em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #9ca3af;&lt;br /&gt;
  margin: 0.1rem 0 0.2rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #e5e7eb;&lt;br /&gt;
  border-radius: 0.35rem;&lt;br /&gt;
  padding: 0.2rem 0.45rem;&lt;br /&gt;
  font-size: 0.78rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  color: #4b5563;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f9fafb;&lt;br /&gt;
  border-color: #d1d5db;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.5rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.5rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.06);&lt;br /&gt;
  padding-top: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.75rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2538</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2538"/>
		<updated>2026-02-22T21:15:46Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 240px;&lt;br /&gt;
  padding: 0.4rem 0.6rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Enhanced Focus State */&lt;br /&gt;
#ohr-search:focus-visible {&lt;br /&gt;
  outline: none;&lt;br /&gt;
  border-color: #3b82f6;&lt;br /&gt;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.85rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.75rem 1rem;&lt;br /&gt;
  box-shadow: 0 1px 3px rgba(0,0,0,0.05);&lt;br /&gt;
  margin-bottom: 0.6rem;&lt;br /&gt;
  border: 1px solid transparent;&lt;br /&gt;
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Container Hover Depth */&lt;br /&gt;
.ohr-item:hover {&lt;br /&gt;
  transform: translateY(-1px);&lt;br /&gt;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);&lt;br /&gt;
  border-color: #e0e7ff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.05rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Refined Metadata Colors */&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.1rem;&lt;br /&gt;
  font-size: 0.85rem;&lt;br /&gt;
  color: #111; /* Value color */&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span.ohr-label {&lt;br /&gt;
  color: #6b7280; /* Softer label color */&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.3rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.4rem 0 0;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  line-height: 1.4;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.6rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link, .ohr-acc-btn {&lt;br /&gt;
  transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.25rem 0.6rem;&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.5rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #dce8f6;&lt;br /&gt;
  border: 1px solid #c8d9ef;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  border-color: #b0c9e8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Focus states for buttons */&lt;br /&gt;
.ohr-link:focus-visible, .ohr-acc-btn:focus-visible {&lt;br /&gt;
  outline: 2px solid #3b82f6;&lt;br /&gt;
  outline-offset: 2px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.35rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.2rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.7rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  margin: 0.15rem 0 0.25rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.25rem 0.5rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { &lt;br /&gt;
  background: #f3f4f6;&lt;br /&gt;
  border-color: #9ca3af;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.6rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.6rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.6rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.75rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2537</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2537"/>
		<updated>2026-02-22T21:11:33Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (Compact CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;&lt;br /&gt;
  margin-bottom: 0.75rem; /* Reduced from 1rem */&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 240px;&lt;br /&gt;
  padding: 0.4rem 0.6rem; /* Slimmer padding */&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.85rem; color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.75rem 1rem; /* Reduced from 1rem 1.25rem */&lt;br /&gt;
  box-shadow: 0 1px 3px rgba(0,0,0,0.05);&lt;br /&gt;
  margin-bottom: 0.6rem; /* Tighter list spacing */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
  align-items: center; /* Centered instead of top-aligned for single-line names */&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.05rem; /* Slightly smaller */&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.2;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.1rem;&lt;br /&gt;
  font-size: 0.85rem; /* Smaller meta text */&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.3rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.4rem 0 0; /* Reduced from 0.65rem */&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  line-height: 1.4;&lt;br /&gt;
  color: #374151;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.6rem; /* Reduced from 0.75rem */&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem; /* Tighter button grouping */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.25rem 0.6rem; /* Even more compact */&lt;br /&gt;
  font-size: 0.8rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  border-radius: 0.5rem; &lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;&lt;br /&gt;
  background: #dce8f6;&lt;br /&gt;
  border: 1px solid #c8d9ef;&lt;br /&gt;
  transition: background 0.1s ease;&lt;br /&gt;
}&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy (Inside Accordion) --- */&lt;br /&gt;
.ohr-session { margin-top: 0.35rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  margin: 0 0 0.2rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.7rem; /* Tiny labels */&lt;br /&gt;
  font-weight: 700;&lt;br /&gt;
  letter-spacing: 0.03em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  margin: 0.15rem 0 0.25rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.25rem 0.5rem;&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.04); }&lt;br /&gt;
&lt;br /&gt;
.ohr-acc-panel { &lt;br /&gt;
  margin-top: 0.6rem; &lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.6rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.6rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.4rem;&lt;br /&gt;
  padding: 0.75rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2536</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2536"/>
		<updated>2026-02-21T22:07:43Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2535</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2535"/>
		<updated>2026-02-21T22:00:03Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2534</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2534"/>
		<updated>2026-02-21T20:55:37Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Sort sessions inside each person by Date From/To year (descending)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
function buildSessionHTML(person, session, idx) {&lt;br /&gt;
  var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
  var title = session.recorded; // No more &amp;quot;(Interview #)&amp;quot; suffix&lt;br /&gt;
&lt;br /&gt;
  // Determine which resource types exist for this session&lt;br /&gt;
  var groups = [&lt;br /&gt;
    { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
    { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
    { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
  ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
  var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
  if (!groups.length) {&lt;br /&gt;
    links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
  } else if (groups.length === 1) {&lt;br /&gt;
    // Single type → no subheading needed; keep on one line (wrap as needed)&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  } else {&lt;br /&gt;
    // Multiple types → labeled groups on separate lines&lt;br /&gt;
    links =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        groups.map(function (g) {&lt;br /&gt;
          return (&lt;br /&gt;
            &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
            &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }).join(&#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      links +&lt;br /&gt;
    &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2533</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2533"/>
		<updated>2026-02-21T20:50:45Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2531</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2531"/>
		<updated>2026-02-21T20:02:27Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Sort sessions inside each person by Date From/To year (descending)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session, idx) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
&lt;br /&gt;
    // If multiple sessions have identical &amp;quot;Recorded YEAR&amp;quot;, add tiny disambiguator:&lt;br /&gt;
    if (person.sessions.length &amp;gt; 1) title = title + &#039; (Interview &#039; + (idx + 1) + &#039;)&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Determine which resource types exist for this session&lt;br /&gt;
    var groups = [&lt;br /&gt;
      { kind: &#039;Video&#039;, urls: session.video },&lt;br /&gt;
      { kind: &#039;Audio&#039;, urls: session.audio },&lt;br /&gt;
      { kind: &#039;Transcript&#039;, urls: session.transcripts }&lt;br /&gt;
    ].filter(function (g) { return g.urls &amp;amp;&amp;amp; g.urls.length; });&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (!groups.length) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else if (groups.length === 1) {&lt;br /&gt;
      // Single type → no subheading needed; keep on one line (wrap as needed)&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          buildLinkListHTML(name, groups[0].kind, groups[0].urls) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      // Multiple types → labeled groups on separate &amp;quot;lines&amp;quot;&lt;br /&gt;
      links =&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-links-stack&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          groups.map(function (g) {&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;div class=&amp;quot;ohr-resource-group&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-resource-label&amp;quot;&amp;gt;&#039; + esc(g.kind) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
                &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
                  buildLinkListHTML(name, g.kind, g.urls) +&lt;br /&gt;
                &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
              &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
            );&lt;br /&gt;
          }).join(&#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2530</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2530"/>
		<updated>2026-02-21T20:01:49Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;&lt;br /&gt;
  margin-bottom: 1rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 280px;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.9rem; color: #6b7280; }&lt;br /&gt;
#ohr-search::placeholder { color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.75rem;&lt;br /&gt;
  padding: 1rem 1.25rem;&lt;br /&gt;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);&lt;br /&gt;
  margin-bottom: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.1rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.25;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.15rem;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span { color: #374151; }&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.4rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.65rem 0 0;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  line-height: 1.45;&lt;br /&gt;
  color: #000;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.75rem; /* increased spacing between buttons */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Tuned, calmer access buttons */&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.32rem 0.7rem;       /* compact but readable */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
  border-radius: 0.75rem;        /* more rounded */&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;                /* a bit darker */&lt;br /&gt;
  background: #dce8f6;           /* slightly darker */&lt;br /&gt;
  border: 1px solid #c8d9ef;     /* subtle but defined */&lt;br /&gt;
  transition: background 0.15s ease, color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Session hierarchy --- */&lt;br /&gt;
.ohr-session { margin-top: 0.4rem; }&lt;br /&gt;
.ohr-session__title {&lt;br /&gt;
  font-size: 1rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  margin: 0.1rem 0 0.35rem;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Resource group label (Audio / Video / Transcript) */&lt;br /&gt;
.ohr-resource-label {&lt;br /&gt;
  font-size: 0.75rem;&lt;br /&gt;
  font-weight: 600;&lt;br /&gt;
  letter-spacing: 0.04em;&lt;br /&gt;
  text-transform: uppercase;&lt;br /&gt;
  color: #6b7280;&lt;br /&gt;
  margin: 0.2rem 0 0.35rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* Stack resource groups vertically when 2+ types exist */&lt;br /&gt;
.ohr-links-stack {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-direction: column;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.03); }&lt;br /&gt;
.ohr-acc-panel { margin-top: 0.8rem; }&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 1rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
  margin-top: 1rem;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2529</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2529"/>
		<updated>2026-02-21T19:57:25Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;&lt;br /&gt;
  margin-bottom: 1rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 280px;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.9rem; color: #6b7280; }&lt;br /&gt;
#ohr-search::placeholder { color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.75rem;&lt;br /&gt;
  padding: 1rem 1.25rem;&lt;br /&gt;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);&lt;br /&gt;
  margin-bottom: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.1rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.25;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.15rem;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span { color: #374151; }&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.4rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.65rem 0 0;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  line-height: 1.45;&lt;br /&gt;
  color: #000;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.75rem;   /* increased from 0.5rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.32rem 0.7rem;       /* slightly larger but still compact */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
  border-radius: 0.75rem;        /* more rounded */&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;                /* darker gray */&lt;br /&gt;
  background: #dce8f6;           /* slightly darker */&lt;br /&gt;
  border: 1px solid #c8d9ef;     /* subtle but defined */&lt;br /&gt;
  transition: background 0.15s ease, color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Chips (Chips view) --- */&lt;br /&gt;
.ohr-chips {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  padding: 0.35rem 0.65rem;&lt;br /&gt;
  border-radius: 9999px;&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  background: #e5eef8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  border: 1px solid #cfe0f3;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip:hover { background: #d6e6f8; }&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.03); }&lt;br /&gt;
.ohr-acc-panel { margin-top: 0.8rem; }&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 1rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
  margin-top: 1rem;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2528</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2528"/>
		<updated>2026-02-21T19:56:53Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;&lt;br /&gt;
  margin-bottom: 1rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 280px;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.9rem; color: #6b7280; }&lt;br /&gt;
#ohr-search::placeholder { color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.75rem;&lt;br /&gt;
  padding: 1rem 1.25rem;&lt;br /&gt;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);&lt;br /&gt;
  margin-bottom: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.1rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.25;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.15rem;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span { color: #374151; }&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.4rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.65rem 0 0;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  line-height: 1.45;&lt;br /&gt;
  color: #000;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.75rem;   /* increased from 0.5rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.32rem 0.7rem;       /* slightly larger but still compact */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 300;&lt;br /&gt;
  border-radius: 0.75rem;        /* more rounded */&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;                /* darker gray */&lt;br /&gt;
  background: #dce8f6;           /* slightly darker */&lt;br /&gt;
  border: 1px solid #c8d9ef;     /* subtle but defined */&lt;br /&gt;
  transition: background 0.15s ease, color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Chips (Chips view) --- */&lt;br /&gt;
.ohr-chips {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  padding: 0.35rem 0.65rem;&lt;br /&gt;
  border-radius: 9999px;&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  background: #e5eef8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  border: 1px solid #cfe0f3;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip:hover { background: #d6e6f8; }&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.03); }&lt;br /&gt;
.ohr-acc-panel { margin-top: 0.8rem; }&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 1rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
  margin-top: 1rem;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2527</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2527"/>
		<updated>2026-02-21T19:55:42Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;&lt;br /&gt;
  margin-bottom: 1rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 280px;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.9rem; color: #6b7280; }&lt;br /&gt;
#ohr-search::placeholder { color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.75rem;&lt;br /&gt;
  padding: 1rem 1.25rem;&lt;br /&gt;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);&lt;br /&gt;
  margin-bottom: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.1rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.25;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.15rem;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span { color: #374151; }&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.4rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.65rem 0 0;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  line-height: 1.45;&lt;br /&gt;
  color: #000;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-links {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.75rem;   /* increased from 0.5rem */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.32rem 0.7rem;       /* slightly larger but still compact */&lt;br /&gt;
  font-size: 0.82rem;&lt;br /&gt;
  font-weight: 500;&lt;br /&gt;
  border-radius: 0.75rem;        /* more rounded */&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #1f2937;                /* darker gray */&lt;br /&gt;
  background: #dce8f6;           /* slightly darker */&lt;br /&gt;
  border: 1px solid #c8d9ef;     /* subtle but defined */&lt;br /&gt;
  transition: background 0.15s ease, color 0.15s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #cfdff3;&lt;br /&gt;
  color: #111827;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Chips (Chips view) --- */&lt;br /&gt;
.ohr-chips {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  padding: 0.35rem 0.65rem;&lt;br /&gt;
  border-radius: 9999px;&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  background: #e5eef8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  border: 1px solid #cfe0f3;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip:hover { background: #d6e6f8; }&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.03); }&lt;br /&gt;
.ohr-acc-panel { margin-top: 0.8rem; }&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 1rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
  margin-top: 1rem;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2526</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.css</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.css&amp;diff=2526"/>
		<updated>2026-02-21T19:54:07Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== Oral History Records (CSS) ===== */&lt;br /&gt;
&lt;br /&gt;
/* --- Layout --- */&lt;br /&gt;
.ohr-list { display: block; }&lt;br /&gt;
&lt;br /&gt;
/* --- Utilities --- */&lt;br /&gt;
.ohr-hidden { display: none; }&lt;br /&gt;
.ohr-muted { color: #6b7280; }&lt;br /&gt;
&lt;br /&gt;
/* --- Toolbar --- */&lt;br /&gt;
.ohr-toolbar {&lt;br /&gt;
  display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center;&lt;br /&gt;
  margin-bottom: 1rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search {&lt;br /&gt;
  flex: 1 1 280px;&lt;br /&gt;
  padding: 0.5rem 0.75rem;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
#ohr-search:focus { outline: 2px solid #9ec5fe; outline-offset: 2px; }&lt;br /&gt;
.ohr-count { margin-left: auto; font-size: 0.9rem; color: #6b7280; }&lt;br /&gt;
#ohr-search::placeholder { color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Record container --- */&lt;br /&gt;
.ohr-item {&lt;br /&gt;
  background: #f5faff;&lt;br /&gt;
  border-radius: 0.75rem;&lt;br /&gt;
  padding: 1rem 1.25rem;&lt;br /&gt;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);&lt;br /&gt;
  margin-bottom: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Header row --- */&lt;br /&gt;
.ohr-head {&lt;br /&gt;
  display: flex;&lt;br /&gt;
  gap: 0.75rem;&lt;br /&gt;
  align-items: flex-start;&lt;br /&gt;
}&lt;br /&gt;
.ohr-titleblock { flex: 1 1 auto; min-width: 0; }&lt;br /&gt;
.ohr-name {&lt;br /&gt;
  margin: 0;&lt;br /&gt;
  font-size: 1.1rem;&lt;br /&gt;
  font-weight: 750;&lt;br /&gt;
  color: #000;&lt;br /&gt;
  line-height: 1.25;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta {&lt;br /&gt;
  margin-top: 0.15rem;&lt;br /&gt;
  font-size: 0.92rem;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
.ohr-meta span { color: #374151; }&lt;br /&gt;
.ohr-meta .ohr-dot { margin: 0 0.4rem; color: #9ca3af; }&lt;br /&gt;
&lt;br /&gt;
/* --- Bio / summary --- */&lt;br /&gt;
.ohr-bio {&lt;br /&gt;
  margin: 0.65rem 0 0;&lt;br /&gt;
  font-size: 0.95rem;&lt;br /&gt;
  line-height: 1.45;&lt;br /&gt;
  color: #000;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Links (Directory &amp;amp; Accordion) --- */&lt;br /&gt;
.ohr-link {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  justify-content: center;&lt;br /&gt;
  padding: 0.25rem 0.55rem;      /* smaller */&lt;br /&gt;
  font-size: 0.82rem;            /* slightly smaller */&lt;br /&gt;
  font-weight: 500;              /* less bold */&lt;br /&gt;
  border-radius: 0.5rem;         /* less pill-like */&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  color: #374151;                /* softer text color */&lt;br /&gt;
  background: #eef4fb;           /* very light background */&lt;br /&gt;
  border: 1px solid #dbe7f5;     /* subtle border */&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.ohr-link:hover {&lt;br /&gt;
  background: #e2edf9;&lt;br /&gt;
  color: #111;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Chips (Chips view) --- */&lt;br /&gt;
.ohr-chips {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  display: flex;&lt;br /&gt;
  flex-wrap: wrap;&lt;br /&gt;
  gap: 0.5rem;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip {&lt;br /&gt;
  display: inline-flex;&lt;br /&gt;
  align-items: center;&lt;br /&gt;
  padding: 0.35rem 0.65rem;&lt;br /&gt;
  border-radius: 9999px;&lt;br /&gt;
  text-decoration: none;&lt;br /&gt;
  font-size: 0.88rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  background: #e5eef8;&lt;br /&gt;
  color: #111;&lt;br /&gt;
  border: 1px solid #cfe0f3;&lt;br /&gt;
}&lt;br /&gt;
.ohr-chip:hover { background: #d6e6f8; }&lt;br /&gt;
&lt;br /&gt;
/* --- Accordion view --- */&lt;br /&gt;
.ohr-acc-btn {&lt;br /&gt;
  margin-left: auto;&lt;br /&gt;
  flex: 0 0 auto;&lt;br /&gt;
  background: transparent;&lt;br /&gt;
  border: 1px solid #d1d5db;&lt;br /&gt;
  border-radius: 0.6rem;&lt;br /&gt;
  padding: 0.35rem 0.6rem;&lt;br /&gt;
  font-size: 0.9rem;&lt;br /&gt;
  font-weight: 650;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
.ohr-acc-btn:hover { background: rgba(0,0,0,0.03); }&lt;br /&gt;
.ohr-acc-panel { margin-top: 0.8rem; }&lt;br /&gt;
.ohr-divider {&lt;br /&gt;
  margin-top: 0.75rem;&lt;br /&gt;
  border-top: 1px solid rgba(0,0,0,0.08);&lt;br /&gt;
  padding-top: 0.75rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/* --- Error --- */&lt;br /&gt;
.ohr-error {&lt;br /&gt;
  color: #b91c1c;&lt;br /&gt;
  background: #fee2e2;&lt;br /&gt;
  border-radius: 0.5rem;&lt;br /&gt;
  padding: 1rem;&lt;br /&gt;
  text-align: center;&lt;br /&gt;
  margin-top: 1rem;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2525</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2525"/>
		<updated>2026-02-21T19:49:25Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip (since you said you made it—this enforces it)&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      // Keep even if only one type exists (common case)&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Optional: sort sessions inside each person by Date From (or recorded year)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        // Descending (newest first)&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var life = lifespan(person);&lt;br /&gt;
    // Lifespan helper expects COL-based row; we’ll compute inline for person object:&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    // Could add “# sessions” but keeping minimalist&lt;br /&gt;
&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session, idx) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
    // If multiple sessions have identical &amp;quot;Recorded YEAR&amp;quot;, add tiny disambiguator:&lt;br /&gt;
    if (person.sessions.length &amp;gt; 1) title = title + &#039; (Interview &#039; + (idx + 1) + &#039;)&#039;;&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Video&#039;, session.video);&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Audio&#039;, session.audio);&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Transcript&#039;, session.transcripts);&lt;br /&gt;
&lt;br /&gt;
    if (!links) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      links = &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + links + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2524</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2524"/>
		<updated>2026-02-21T19:47:35Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2523</id>
		<title>File:OHData.csv</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=File:OHData.csv&amp;diff=2523"/>
		<updated>2026-02-21T19:29:32Z</updated>

		<summary type="html">&lt;p&gt;Hthach: Hthach uploaded a new version of File:OHData.csv&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2522</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2522"/>
		<updated>2026-02-21T19:28:50Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js (Grouped by Person Key, Option 2) ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG (MUST MATCH CSV HEADERS EXACTLY) ======&lt;br /&gt;
  var COL = {&lt;br /&gt;
    personKey: &#039;Person Key&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;,&lt;br /&gt;
    audio: &#039;Audio links&#039;,&lt;br /&gt;
    video: &#039;Video links&#039;,&lt;br /&gt;
    transcripts: &#039;Transcripts links&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var PEOPLE = [];              // grouped person records&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(row) {&lt;br /&gt;
    var y1 = toYear(row[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(row[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;Recorded (date unknown)&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(row) {&lt;br /&gt;
    var first = (row[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (row[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(row) {&lt;br /&gt;
    var b = (row[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (row[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Extract URLs from a cell that may include commentary, newlines, semicolons, etc.&lt;br /&gt;
  // We only call this for link columns.&lt;br /&gt;
  function extractUrls(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    var s = String(value);&lt;br /&gt;
&lt;br /&gt;
    // Match http/https URLs up to whitespace/quote/angle bracket&lt;br /&gt;
    var re = /https?:\/\/[^\s&amp;quot;&#039;&amp;lt;&amp;gt;()]+/g;&lt;br /&gt;
    var m = s.match(re) || [];&lt;br /&gt;
&lt;br /&gt;
    // De-dupe while preserving order&lt;br /&gt;
    var seen = Object.create(null);&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; m.length; i++) {&lt;br /&gt;
      var url = m[i].trim();&lt;br /&gt;
      // Strip trailing punctuation that often sticks to URLs&lt;br /&gt;
      url = url.replace(/[);.,]+$/g, &#039;&#039;);&lt;br /&gt;
      if (!url) continue;&lt;br /&gt;
      if (!seen[url]) {&lt;br /&gt;
        seen[url] = true;&lt;br /&gt;
        out.push(url);&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles quotes, commas in quotes, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== GROUPING ======&lt;br /&gt;
  function firstNonEmpty(a, b) {&lt;br /&gt;
    var A = (a || &#039;&#039;).trim();&lt;br /&gt;
    if (A) return A;&lt;br /&gt;
    return (b || &#039;&#039;).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function groupByPersonKey(rows) {&lt;br /&gt;
    var map = Object.create(null);&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      var row = rows[i];&lt;br /&gt;
      var key = (row[COL.personKey] || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
      // If Person Key is missing, skip (since you said you made it—this enforces it)&lt;br /&gt;
      if (!key) continue;&lt;br /&gt;
&lt;br /&gt;
      // Also require full name to avoid junk records&lt;br /&gt;
      if (!fullName(row)) continue;&lt;br /&gt;
&lt;br /&gt;
      if (!map[key]) {&lt;br /&gt;
        map[key] = {&lt;br /&gt;
          key: key,&lt;br /&gt;
          first: (row[COL.first] || &#039;&#039;).trim(),&lt;br /&gt;
          last: (row[COL.last] || &#039;&#039;).trim(),&lt;br /&gt;
          birth: (row[COL.birth] || &#039;&#039;).trim(),&lt;br /&gt;
          death: (row[COL.death] || &#039;&#039;).trim(),&lt;br /&gt;
          wiki: (row[COL.wiki] || &#039;&#039;).trim(),&lt;br /&gt;
          bio: (row[COL.bio] || &#039;&#039;).trim(),&lt;br /&gt;
          sessions: []&lt;br /&gt;
        };&lt;br /&gt;
      } else {&lt;br /&gt;
        // Fill any missing person-level fields from other rows&lt;br /&gt;
        map[key].first = firstNonEmpty(map[key].first, row[COL.first]);&lt;br /&gt;
        map[key].last = firstNonEmpty(map[key].last, row[COL.last]);&lt;br /&gt;
        map[key].birth = firstNonEmpty(map[key].birth, row[COL.birth]);&lt;br /&gt;
        map[key].death = firstNonEmpty(map[key].death, row[COL.death]);&lt;br /&gt;
        map[key].wiki = firstNonEmpty(map[key].wiki, row[COL.wiki]);&lt;br /&gt;
        map[key].bio = firstNonEmpty(map[key].bio, row[COL.bio]);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      // Session links: extract URLs ONLY from the 3 link columns&lt;br /&gt;
      var session = {&lt;br /&gt;
        recorded: recordedLabel(row),&lt;br /&gt;
        dateFrom: (row[COL.dateFrom] || &#039;&#039;).trim(),&lt;br /&gt;
        dateTo: (row[COL.dateTo] || &#039;&#039;).trim(),&lt;br /&gt;
        video: extractUrls(row[COL.video]),&lt;br /&gt;
        audio: extractUrls(row[COL.audio]),&lt;br /&gt;
        transcripts: extractUrls(row[COL.transcripts])&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      // Keep even if only one type exists (common case)&lt;br /&gt;
      map[key].sessions.push(session);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Convert to array and sort by last, first&lt;br /&gt;
    var people = Object.keys(map).map(function (k) { return map[k]; });&lt;br /&gt;
&lt;br /&gt;
    people.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a.last || &#039;&#039;) + &#039; &#039; + (a.first || &#039;&#039;));&lt;br /&gt;
      var B = norm((b.last || &#039;&#039;) + &#039; &#039; + (b.first || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // Optional: sort sessions inside each person by Date From (or recorded year)&lt;br /&gt;
    people.forEach(function (p) {&lt;br /&gt;
      p.sessions.sort(function (s1, s2) {&lt;br /&gt;
        var y1 = toYear(s1.dateFrom) || toYear(s1.dateTo) || &#039;&#039;;&lt;br /&gt;
        var y2 = toYear(s2.dateFrom) || toYear(s2.dateTo) || &#039;&#039;;&lt;br /&gt;
        // Descending (newest first)&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; y2) return Number(y2) - Number(y1);&lt;br /&gt;
        if (y1 &amp;amp;&amp;amp; !y2) return -1;&lt;br /&gt;
        if (!y1 &amp;amp;&amp;amp; y2) return 1;&lt;br /&gt;
        return 0;&lt;br /&gt;
      });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    return people;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== RENDERING (OPTION 2) ======&lt;br /&gt;
  function buildPersonHeaderHTML(person) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var life = lifespan(person);&lt;br /&gt;
    // Lifespan helper expects COL-based row; we’ll compute inline for person object:&lt;br /&gt;
    var b = (person.birth || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person.death || &#039;&#039;).trim();&lt;br /&gt;
    var lifeStr = &#039;&#039;;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) lifeStr = b + &#039;–&#039; + d;&lt;br /&gt;
    else if (b &amp;amp;&amp;amp; !d) lifeStr = b + &#039;–&#039;;&lt;br /&gt;
    else if (!b &amp;amp;&amp;amp; d) lifeStr = &#039;–&#039; + d;&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (lifeStr) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(lifeStr) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    // Could add “# sessions” but keeping minimalist&lt;br /&gt;
&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
        (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(url) {&lt;br /&gt;
    url = (url || &#039;&#039;).trim();&lt;br /&gt;
    if (!url) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(url) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinkListHTML(personName, kind, urls) {&lt;br /&gt;
    if (!urls || !urls.length) return &#039;&#039;;&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
      var label = &#039;Access &#039; + personName + &#039;’s &#039; + kind +&lt;br /&gt;
        (urls.length &amp;gt; 1 ? (&#039; (Part &#039; + (i + 1) + &#039;/&#039; + urls.length + &#039;)&#039;) : &#039;&#039;);&lt;br /&gt;
      out.push(&lt;br /&gt;
        &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          esc(label) +&lt;br /&gt;
        &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
      );&lt;br /&gt;
    }&lt;br /&gt;
    return out.join(&#039;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildSessionHTML(person, session, idx) {&lt;br /&gt;
    var name = ((person.first || &#039;&#039;).trim() + &#039; &#039; + (person.last || &#039;&#039;).trim()).trim();&lt;br /&gt;
    var title = session.recorded;&lt;br /&gt;
    // If multiple sessions have identical &amp;quot;Recorded YEAR&amp;quot;, add tiny disambiguator:&lt;br /&gt;
    if (person.sessions.length &amp;gt; 1) title = title + &#039; (Interview &#039; + (idx + 1) + &#039;)&#039;;&lt;br /&gt;
&lt;br /&gt;
    var links = &#039;&#039;;&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Video&#039;, session.video);&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Audio&#039;, session.audio);&lt;br /&gt;
    links += buildLinkListHTML(name, &#039;Transcript&#039;, session.transcripts);&lt;br /&gt;
&lt;br /&gt;
    if (!links) {&lt;br /&gt;
      links = &#039;&amp;lt;span class=&amp;quot;ohr-muted&amp;quot;&amp;gt;No links available for this session.&amp;lt;/span&amp;gt;&#039;;&lt;br /&gt;
    } else {&lt;br /&gt;
      links = &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + links + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-session&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-session__title&amp;quot;&amp;gt;&#039; + esc(title) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        links +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionPerson(person, idx) {&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    var wikiLink = buildWikiLinkHTML(person.wiki);&lt;br /&gt;
    var bio = (person.bio || &#039;&#039;).trim();&lt;br /&gt;
&lt;br /&gt;
    var sessionsHTML = &#039;&#039;;&lt;br /&gt;
    for (var i = 0; i &amp;lt; person.sessions.length; i++) {&lt;br /&gt;
      sessionsHTML += buildSessionHTML(person, person.sessions[i], i);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        buildPersonHeaderHTML(person) +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        (wikiLink ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + wikiLink + &#039;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + sessionsHTML + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderAccordion(people) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!people.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
    for (var i = 0; i &amp;lt; people.length; i++) {&lt;br /&gt;
      frag.appendChild(makeAccordionPerson(people[i], i));&lt;br /&gt;
    }&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; people&#039;) : (n + &#039; of &#039; + total + &#039; people&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== FILTERING ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    var filtered = PEOPLE.filter(function (p) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (p.first || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.last || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.bio || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.birth || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.death || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (p.wiki || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
&lt;br /&gt;
      // Also include session recorded labels in search (years)&lt;br /&gt;
      for (var i = 0; i &amp;lt; p.sessions.length; i++) {&lt;br /&gt;
        hay += &#039; &#039; + norm(p.sessions[i].recorded || &#039;&#039;);&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderAccordion(filtered);&lt;br /&gt;
    updateCount(filtered.length, PEOPLE.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ERRORS ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== MAIN ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        var rows = parseCSV(text);&lt;br /&gt;
        PEOPLE = groupByPersonKey(rows);&lt;br /&gt;
&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters(); // initial render&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2521</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2521"/>
		<updated>2026-02-21T18:45:10Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG ======&lt;br /&gt;
  // These must match your CSV header names exactly.&lt;br /&gt;
  var COL = {&lt;br /&gt;
    transcripts: &#039;Transcript Link&#039;,&lt;br /&gt;
    audio: &#039;Audio Link&#039;,&lt;br /&gt;
    video: &#039;Video Link&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    bio: &#039;Short Bio&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var ALL = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Pattern 1: page controls view via &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-view=&amp;quot;...&amp;quot;&amp;gt;&lt;br /&gt;
  function getViewMode() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var mode = (root &amp;amp;&amp;amp; root.getAttribute(&#039;data-view&#039;)) ? root.getAttribute(&#039;data-view&#039;).trim().toLowerCase() : &#039;&#039;;&lt;br /&gt;
    if (mode === &#039;accordion&#039; || mode === &#039;chips&#039; || mode === &#039;directory&#039;) return mode;&lt;br /&gt;
    return &#039;directory&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    // wgScript is usually &amp;quot;/index.php&amp;quot; on your site (matches your working URL style)&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(person) {&lt;br /&gt;
    var y1 = toYear(person[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(person[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // IMPORTANT: Only split on commas for the 3 link columns (Audio/Video/Transcripts).&lt;br /&gt;
  // Other columns may contain commas as normal punctuation.&lt;br /&gt;
  function splitLinkCell(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    return String(value)&lt;br /&gt;
      .split(&#039;,&#039;)&lt;br /&gt;
      .map(function (x) { return x.trim(); })&lt;br /&gt;
      .filter(Boolean);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Must have both first &amp;amp; last name&lt;br /&gt;
  function hasValidName(person) {&lt;br /&gt;
    var first = (person[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (person[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    return !!(first &amp;amp;&amp;amp; last);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(person) {&lt;br /&gt;
    var first = (person[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (person[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    return (first &amp;amp;&amp;amp; last) ? (first + &#039; &#039; + last) : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(person) {&lt;br /&gt;
    var b = (person[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Directory + Chips: truncate to 300 chars.&lt;br /&gt;
  // Accordion: full bio.&lt;br /&gt;
  function truncate(text, maxChars) {&lt;br /&gt;
    if (!text) return &#039;&#039;;&lt;br /&gt;
    var s = String(text).trim();&lt;br /&gt;
    if (s.length &amp;lt;= maxChars) return s;&lt;br /&gt;
    return s.slice(0, maxChars).trim() + &#039;…&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildAccessLabel(name, kind, idx, total) {&lt;br /&gt;
    var part = (total &amp;gt; 1) ? (&#039; (Part &#039; + (idx + 1) + &#039;/&#039; + total + &#039;)&#039;) : &#039;&#039;;&lt;br /&gt;
    var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);&lt;br /&gt;
    return &#039;Access &#039; + name + &#039;’s &#039; + kindTitle + part;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildChipText(kind, idx, total) {&lt;br /&gt;
    var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);&lt;br /&gt;
    if (total &amp;gt; 1) return kindTitle + &#039; &#039; + (idx + 1) + &#039;/&#039; + total;&lt;br /&gt;
    return kindTitle;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, bios…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles: quotes, embedded commas, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Rendering building blocks ======&lt;br /&gt;
  function buildHeaderHTML(person) {&lt;br /&gt;
    var name = fullName(person);&lt;br /&gt;
    var life = lifespan(person);&lt;br /&gt;
    var rec = recordedLabel(person);&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (life) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(life) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    if (rec) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(rec) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
          (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(person, cls) {&lt;br /&gt;
    var wiki = (person[COL.wiki] || &#039;&#039;).trim();&lt;br /&gt;
    if (!wiki) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;&#039; + cls + &#039;&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(wiki) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinksHTML(person, style) {&lt;br /&gt;
    var name = fullName(person);&lt;br /&gt;
&lt;br /&gt;
    var video = splitLinkCell(person[COL.video]);&lt;br /&gt;
    var audio = splitLinkCell(person[COL.audio]);&lt;br /&gt;
    var transcripts = splitLinkCell(person[COL.transcripts]);&lt;br /&gt;
&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    function add(kind, urls) {&lt;br /&gt;
      for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
        var label = buildAccessLabel(name, kind, i, urls.length);&lt;br /&gt;
&lt;br /&gt;
        if (style === &#039;chips&#039;) {&lt;br /&gt;
          var chipText = buildChipText(kind, i, urls.length);&lt;br /&gt;
          out.push(&lt;br /&gt;
            &#039;&amp;lt;a class=&amp;quot;ohr-chip&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&#039; +&lt;br /&gt;
              &#039; title=&amp;quot;&#039; + esc(label) + &#039;&amp;quot;&#039; +&lt;br /&gt;
              &#039; aria-label=&amp;quot;&#039; + esc(label) + &#039;&amp;quot;&#039; +&lt;br /&gt;
            &#039;&amp;gt;&#039; + esc(chipText) + &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        } else {&lt;br /&gt;
          out.push(&lt;br /&gt;
            &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              esc(label) +&lt;br /&gt;
            &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Wiki link first (if present)&lt;br /&gt;
    if (style === &#039;chips&#039;) {&lt;br /&gt;
      var wikiChip = buildWikiLinkHTML(person, &#039;ohr-chip&#039;);&lt;br /&gt;
      if (wikiChip) out.push(wikiChip);&lt;br /&gt;
    } else {&lt;br /&gt;
      var wikiLink = buildWikiLinkHTML(person, &#039;ohr-link&#039;);&lt;br /&gt;
      if (wikiLink) out.push(wikiLink);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (video.length) add(&#039;video&#039;, video);&lt;br /&gt;
    if (audio.length) add(&#039;audio&#039;, audio);&lt;br /&gt;
    if (transcripts.length) add(&#039;transcript&#039;, transcripts);&lt;br /&gt;
&lt;br /&gt;
    if (!out.length) return &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (style === &#039;chips&#039;) return &#039;&amp;lt;div class=&amp;quot;ohr-chips&amp;quot;&amp;gt;&#039; + out.join(&#039;&#039;) + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + out.join(&#039;&#039;) + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Views ======&lt;br /&gt;
  function makeDirectoryItem(person) {&lt;br /&gt;
    var bio = truncate((person[COL.bio] || &#039;&#039;).trim(), 300); // truncated&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      buildHeaderHTML(person) +&lt;br /&gt;
      (bio ? &#039;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      buildLinksHTML(person, &#039;links&#039;);&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionItem(person, idx) {&lt;br /&gt;
    var bio = (person[COL.bio] || &#039;&#039;).trim(); // full bio (Option 2)&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          (function () {&lt;br /&gt;
            var name = fullName(person);&lt;br /&gt;
            var life = lifespan(person);&lt;br /&gt;
            var rec = recordedLabel(person);&lt;br /&gt;
&lt;br /&gt;
            var metaBits = [];&lt;br /&gt;
            if (life) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(life) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
            if (rec) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(rec) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
            var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
              (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;)&lt;br /&gt;
            );&lt;br /&gt;
          })() +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (bio ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + buildLinksHTML(person, &#039;links&#039;) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeChipsItem(person) {&lt;br /&gt;
    var bio = truncate((person[COL.bio] || &#039;&#039;).trim(), 300); // truncated&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      buildHeaderHTML(person) +&lt;br /&gt;
      (bio ? &#039;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot;&amp;gt;&#039; + esc(bio) + &#039;&amp;lt;/p&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      buildLinksHTML(person, &#039;chips&#039;);&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderList(rows) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var mode = getViewMode();&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      if (mode === &#039;accordion&#039;) frag.appendChild(makeAccordionItem(rows[i], i));&lt;br /&gt;
      else if (mode === &#039;chips&#039;) frag.appendChild(makeChipsItem(rows[i]));&lt;br /&gt;
      else frag.appendChild(makeDirectoryItem(rows[i]));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;); if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; records&#039;) : (n + &#039; of &#039; + total + &#039; records&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Filtering ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    // Remove invalid-name records entirely&lt;br /&gt;
    var valid = ALL.filter(function (row) {&lt;br /&gt;
      return hasValidName(row);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    var filtered = valid.filter(function (row) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (row[COL.first] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.last] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.bio] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.birth] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.death] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.dateFrom] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.dateTo] || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    filtered.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a[COL.last] || &#039;&#039;) + &#039; &#039; + (a[COL.first] || &#039;&#039;));&lt;br /&gt;
      var B = norm((b[COL.last] || &#039;&#039;) + &#039; &#039; + (b[COL.first] || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderList(filtered);&lt;br /&gt;
    updateCount(filtered.length, valid.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Errors ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Main ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        ALL = parseCSV(text);&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
	<entry>
		<id>https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2517</id>
		<title>MediaWiki:Gadget-OralHistoryRecords-Updated.js</title>
		<link rel="alternate" type="text/html" href="https://asist.a2hosted.com/index.php?title=MediaWiki:Gadget-OralHistoryRecords-Updated.js&amp;diff=2517"/>
		<updated>2026-02-21T18:29:55Z</updated>

		<summary type="html">&lt;p&gt;Hthach: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* ===== OralHistoryRecords-Updated.js ===== */&lt;br /&gt;
/* global mw */&lt;br /&gt;
(function () {&lt;br /&gt;
  &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
  // ====== COLUMN CONFIG ======&lt;br /&gt;
  // These must match your CSV header names exactly.&lt;br /&gt;
  var COL = {&lt;br /&gt;
    transcript: &#039;Transcript Identifier&#039;,&lt;br /&gt;
    audio: &#039;Audio Identifier&#039;,&lt;br /&gt;
    video: &#039;Video Identifier&#039;,&lt;br /&gt;
    first: &#039;First Name&#039;,&lt;br /&gt;
    last: &#039;Last Name&#039;,&lt;br /&gt;
    birth: &#039;Birth Year&#039;,&lt;br /&gt;
    death: &#039;Death Year&#039;,&lt;br /&gt;
    dateFrom: &#039;Date From&#039;,&lt;br /&gt;
    dateTo: &#039;Date To&#039;,&lt;br /&gt;
    summary: &#039;Summary&#039;,&lt;br /&gt;
    wiki: &#039;Wiki Link&#039;&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ====== STATE ======&lt;br /&gt;
  var ALL = [];&lt;br /&gt;
  var FILTER = { q: &#039;&#039; };&lt;br /&gt;
&lt;br /&gt;
  // ====== UTILS ======&lt;br /&gt;
  function onReady(fn) {&lt;br /&gt;
    if (document.readyState !== &#039;loading&#039;) fn();&lt;br /&gt;
    else document.addEventListener(&#039;DOMContentLoaded&#039;, fn);&lt;br /&gt;
  }&lt;br /&gt;
  function $(id) { return document.getElementById(id); }&lt;br /&gt;
  function esc(s) {&lt;br /&gt;
    s = (s == null ? &#039;&#039; : String(s));&lt;br /&gt;
    return s.replace(/[&amp;amp;&amp;lt;&amp;gt;&amp;quot;&#039;]/g, function (c) {&lt;br /&gt;
      return ({ &#039;&amp;amp;&#039;: &#039;&amp;amp;amp;&#039;, &#039;&amp;lt;&#039;: &#039;&amp;amp;lt;&#039;, &#039;&amp;gt;&#039;: &#039;&amp;amp;gt;&#039;, &#039;&amp;quot;&#039;: &#039;&amp;amp;quot;&#039;, &amp;quot;&#039;&amp;quot;: &#039;&amp;amp;#39;&#039; }[c]);&lt;br /&gt;
    });&lt;br /&gt;
  }&lt;br /&gt;
  function norm(s) {&lt;br /&gt;
    return (s || &#039;&#039;)&lt;br /&gt;
      .toString()&lt;br /&gt;
      .normalize(&#039;NFD&#039;)&lt;br /&gt;
      .replace(/[\u0300-\u036f]/g, &#039;&#039;)&lt;br /&gt;
      .toLowerCase();&lt;br /&gt;
  }&lt;br /&gt;
  function debounce(fn, ms) {&lt;br /&gt;
    var t;&lt;br /&gt;
    return function () {&lt;br /&gt;
      var a = arguments, self = this;&lt;br /&gt;
      clearTimeout(t);&lt;br /&gt;
      t = setTimeout(function () { fn.apply(self, a); }, ms);&lt;br /&gt;
    };&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Pattern 1: page controls view via &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-view=&amp;quot;...&amp;quot;&amp;gt;&lt;br /&gt;
  function getViewMode() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var mode = (root &amp;amp;&amp;amp; root.getAttribute(&#039;data-view&#039;)) ? root.getAttribute(&#039;data-view&#039;).trim().toLowerCase() : &#039;&#039;;&lt;br /&gt;
    if (mode === &#039;accordion&#039; || mode === &#039;chips&#039; || mode === &#039;directory&#039;) return mode;&lt;br /&gt;
    return &#039;directory&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // Optional override:&lt;br /&gt;
  // &amp;lt;div id=&amp;quot;ohr-directory&amp;quot; data-csv=&amp;quot;/index.php/Special:FilePath/OHData.csv&amp;quot;&amp;gt;&lt;br /&gt;
  function getCsvUrl() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var override = root ? (root.getAttribute(&#039;data-csv&#039;) || &#039;&#039;).trim() : &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    // wgScript is usually &amp;quot;/index.php&amp;quot; on your site (matches your working URL style)&lt;br /&gt;
    var base = override || ((mw.config.get(&#039;wgScript&#039;) || &#039;/index.php&#039;) + &#039;/Special:FilePath/OHData.csv&#039;);&lt;br /&gt;
&lt;br /&gt;
    return base + (base.indexOf(&#039;?&#039;) === -1 ? &#039;?&#039; : &#039;&amp;amp;&#039;) + &#039;cb=&#039; + Date.now();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function toYear(value) {&lt;br /&gt;
    if (!value) return &#039;&#039;;&lt;br /&gt;
    var s = String(value).trim();&lt;br /&gt;
    var m = s.match(/\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b/);&lt;br /&gt;
    return m ? m[1] : &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function recordedLabel(person) {&lt;br /&gt;
    var y1 = toYear(person[COL.dateFrom]);&lt;br /&gt;
    var y2 = toYear(person[COL.dateTo]);&lt;br /&gt;
    if (y1 &amp;amp;&amp;amp; y2) return (y1 === y2) ? (&#039;Recorded &#039; + y1) : (&#039;Recorded &#039; + y1 + &#039;–&#039; + y2);&lt;br /&gt;
    if (y1) return &#039;Recorded &#039; + y1;&lt;br /&gt;
    if (y2) return &#039;Recorded &#039; + y2;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function splitLinksCSV(value) {&lt;br /&gt;
    if (!value) return [];&lt;br /&gt;
    return String(value)&lt;br /&gt;
      .split(&#039;,&#039;)&lt;br /&gt;
      .map(function (x) { return x.trim(); })&lt;br /&gt;
      .filter(Boolean);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // NEW RULE: must have both first and last name to be valid&lt;br /&gt;
  function hasValidName(person) {&lt;br /&gt;
    var first = (person[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (person[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    return !!(first &amp;amp;&amp;amp; last);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function fullName(person) {&lt;br /&gt;
    var first = (person[COL.first] || &#039;&#039;).trim();&lt;br /&gt;
    var last = (person[COL.last] || &#039;&#039;).trim();&lt;br /&gt;
    if (!first || !last) return &#039;&#039;;&lt;br /&gt;
    return (first + &#039; &#039; + last).trim();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function lifespan(person) {&lt;br /&gt;
    var b = (person[COL.birth] || &#039;&#039;).trim();&lt;br /&gt;
    var d = (person[COL.death] || &#039;&#039;).trim();&lt;br /&gt;
    if (b &amp;amp;&amp;amp; d) return b + &#039;–&#039; + d;&lt;br /&gt;
    if (b &amp;amp;&amp;amp; !d) return b + &#039;–&#039;;&lt;br /&gt;
    if (!b &amp;amp;&amp;amp; d) return &#039;–&#039; + d;&lt;br /&gt;
    return &#039;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // NEW RULE: summary limited to 300 chars&lt;br /&gt;
  function truncateSummary(text, maxChars) {&lt;br /&gt;
    if (!text) return &#039;&#039;;&lt;br /&gt;
    var s = String(text).trim();&lt;br /&gt;
    if (s.length &amp;lt;= maxChars) return s;&lt;br /&gt;
    return s.slice(0, maxChars).trim() + &#039;…&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildAccessLabel(name, kind, idx, total) {&lt;br /&gt;
    var part = (total &amp;gt; 1) ? (&#039; (Part &#039; + (idx + 1) + &#039;/&#039; + total + &#039;)&#039;) : &#039;&#039;;&lt;br /&gt;
    var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);&lt;br /&gt;
    return &#039;Access &#039; + name + &#039;’s &#039; + kindTitle + part;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildChipText(kind, idx, total) {&lt;br /&gt;
    var kindTitle = kind.charAt(0).toUpperCase() + kind.slice(1);&lt;br /&gt;
    if (total &amp;gt; 1) return kindTitle + &#039; &#039; + (idx + 1) + &#039;/&#039; + total;&lt;br /&gt;
    return kindTitle;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== UI ======&lt;br /&gt;
  function buildToolbar(root) {&lt;br /&gt;
    var toolbar = document.createElement(&#039;div&#039;);&lt;br /&gt;
    toolbar.className = &#039;ohr-toolbar&#039;;&lt;br /&gt;
&lt;br /&gt;
    var search = document.createElement(&#039;input&#039;);&lt;br /&gt;
    search.id = &#039;ohr-search&#039;;&lt;br /&gt;
    search.type = &#039;search&#039;;&lt;br /&gt;
    search.placeholder = &#039;Search names, summaries…&#039;;&lt;br /&gt;
    search.setAttribute(&#039;aria-label&#039;, &#039;Search oral history records&#039;);&lt;br /&gt;
&lt;br /&gt;
    var count = document.createElement(&#039;div&#039;);&lt;br /&gt;
    count.id = &#039;ohr-count&#039;;&lt;br /&gt;
    count.className = &#039;ohr-count&#039;;&lt;br /&gt;
    count.setAttribute(&#039;aria-live&#039;, &#039;polite&#039;);&lt;br /&gt;
&lt;br /&gt;
    toolbar.appendChild(search);&lt;br /&gt;
    toolbar.appendChild(count);&lt;br /&gt;
    root.insertBefore(toolbar, root.firstChild);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== ROBUST CSV PARSER ======&lt;br /&gt;
  // Handles: quotes, embedded commas, embedded newlines, escaped quotes (&amp;quot;&amp;quot;)&lt;br /&gt;
  function parseCSV(text) {&lt;br /&gt;
    if (!text) return [];&lt;br /&gt;
&lt;br /&gt;
    text = String(text).replace(/\r\n/g, &#039;\n&#039;).replace(/\r/g, &#039;\n&#039;);&lt;br /&gt;
&lt;br /&gt;
    var rows = [];&lt;br /&gt;
    var row = [];&lt;br /&gt;
    var field = &#039;&#039;;&lt;br /&gt;
    var i = 0;&lt;br /&gt;
    var inQuotes = false;&lt;br /&gt;
&lt;br /&gt;
    while (i &amp;lt; text.length) {&lt;br /&gt;
      var c = text[i];&lt;br /&gt;
&lt;br /&gt;
      if (inQuotes) {&lt;br /&gt;
        if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
          if (i + 1 &amp;lt; text.length &amp;amp;&amp;amp; text[i + 1] === &#039;&amp;quot;&#039;) {&lt;br /&gt;
            field += &#039;&amp;quot;&#039;;&lt;br /&gt;
            i += 2;&lt;br /&gt;
            continue;&lt;br /&gt;
          }&lt;br /&gt;
          inQuotes = false;&lt;br /&gt;
          i += 1;&lt;br /&gt;
          continue;&lt;br /&gt;
        }&lt;br /&gt;
        field += c;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      if (c === &#039;&amp;quot;&#039;) {&lt;br /&gt;
        inQuotes = true;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;,&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
      if (c === &#039;\n&#039;) {&lt;br /&gt;
        row.push(field);&lt;br /&gt;
        field = &#039;&#039;;&lt;br /&gt;
        rows.push(row);&lt;br /&gt;
        row = [];&lt;br /&gt;
        i += 1;&lt;br /&gt;
        continue;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      field += c;&lt;br /&gt;
      i += 1;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    row.push(field);&lt;br /&gt;
    if (row.length &amp;gt; 1 || (row.length === 1 &amp;amp;&amp;amp; row[0] !== &#039;&#039;)) rows.push(row);&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) return [];&lt;br /&gt;
&lt;br /&gt;
    var headers = rows[0].map(function (h) { return (h || &#039;&#039;).trim(); });&lt;br /&gt;
&lt;br /&gt;
    var out = [];&lt;br /&gt;
    for (var r = 1; r &amp;lt; rows.length; r++) {&lt;br /&gt;
      var values = rows[r];&lt;br /&gt;
&lt;br /&gt;
      // Skip totally empty rows&lt;br /&gt;
      var nonEmpty = false;&lt;br /&gt;
      for (var k = 0; k &amp;lt; values.length; k++) {&lt;br /&gt;
        if (String(values[k] || &#039;&#039;).trim() !== &#039;&#039;) { nonEmpty = true; break; }&lt;br /&gt;
      }&lt;br /&gt;
      if (!nonEmpty) continue;&lt;br /&gt;
&lt;br /&gt;
      var obj = {};&lt;br /&gt;
      for (var c2 = 0; c2 &amp;lt; headers.length; c2++) {&lt;br /&gt;
        var key = headers[c2];&lt;br /&gt;
        if (!key) continue;&lt;br /&gt;
        obj[key] = (c2 &amp;lt; values.length) ? values[c2] : &#039;&#039;;&lt;br /&gt;
      }&lt;br /&gt;
      out.push(obj);&lt;br /&gt;
    }&lt;br /&gt;
    return out;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Rendering building blocks ======&lt;br /&gt;
  function buildHeaderHTML(person) {&lt;br /&gt;
    var name = fullName(person);&lt;br /&gt;
    var life = lifespan(person);&lt;br /&gt;
    var rec = recordedLabel(person);&lt;br /&gt;
&lt;br /&gt;
    var metaBits = [];&lt;br /&gt;
    if (life) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(life) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
    if (rec) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(rec) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
    return (&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
          (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;&lt;br /&gt;
    );&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildWikiLinkHTML(person, cls) {&lt;br /&gt;
    var wiki = (person[COL.wiki] || &#039;&#039;).trim();&lt;br /&gt;
    if (!wiki) return &#039;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;a class=&amp;quot;&#039; + cls + &#039;&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(wiki) + &#039;&amp;quot;&amp;gt;Read Wiki Page&amp;lt;/a&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function buildLinksHTML(person, style) {&lt;br /&gt;
    var name = fullName(person);&lt;br /&gt;
&lt;br /&gt;
    var video = splitLinksCSV(person[COL.video]);&lt;br /&gt;
    var audio = splitLinksCSV(person[COL.audio]);&lt;br /&gt;
    var transcript = splitLinksCSV(person[COL.transcript]);&lt;br /&gt;
&lt;br /&gt;
    var out = [];&lt;br /&gt;
&lt;br /&gt;
    function add(kind, urls) {&lt;br /&gt;
      for (var i = 0; i &amp;lt; urls.length; i++) {&lt;br /&gt;
        var label = buildAccessLabel(name, kind, i, urls.length);&lt;br /&gt;
&lt;br /&gt;
        if (style === &#039;chips&#039;) {&lt;br /&gt;
          var chipText = buildChipText(kind, i, urls.length);&lt;br /&gt;
          out.push(&lt;br /&gt;
            &#039;&amp;lt;a class=&amp;quot;ohr-chip&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&#039; +&lt;br /&gt;
              &#039; title=&amp;quot;&#039; + esc(label) + &#039;&amp;quot;&#039; +&lt;br /&gt;
              &#039; aria-label=&amp;quot;&#039; + esc(label) + &#039;&amp;quot;&#039; +&lt;br /&gt;
            &#039;&amp;gt;&#039; + esc(chipText) + &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        } else {&lt;br /&gt;
          out.push(&lt;br /&gt;
            &#039;&amp;lt;a class=&amp;quot;ohr-link&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener&amp;quot; href=&amp;quot;&#039; + esc(urls[i]) + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
              esc(label) +&lt;br /&gt;
            &#039;&amp;lt;/a&amp;gt;&#039;&lt;br /&gt;
          );&lt;br /&gt;
        }&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Wiki link first (if present)&lt;br /&gt;
    if (style === &#039;chips&#039;) {&lt;br /&gt;
      var wikiChip = buildWikiLinkHTML(person, &#039;ohr-chip&#039;);&lt;br /&gt;
      if (wikiChip) out.push(wikiChip);&lt;br /&gt;
    } else {&lt;br /&gt;
      var wikiLink = buildWikiLinkHTML(person, &#039;ohr-link&#039;);&lt;br /&gt;
      if (wikiLink) out.push(wikiLink);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (video.length) add(&#039;video&#039;, video);&lt;br /&gt;
    if (audio.length) add(&#039;audio&#039;, audio);&lt;br /&gt;
    if (transcript.length) add(&#039;transcript&#039;, transcript);&lt;br /&gt;
&lt;br /&gt;
    if (!out.length) return &#039;&#039;;&lt;br /&gt;
&lt;br /&gt;
    if (style === &#039;chips&#039;) return &#039;&amp;lt;div class=&amp;quot;ohr-chips&amp;quot;&amp;gt;&#039; + out.join(&#039;&#039;) + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
    return &#039;&amp;lt;div class=&amp;quot;ohr-links&amp;quot;&amp;gt;&#039; + out.join(&#039;&#039;) + &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Views ======&lt;br /&gt;
  function makeDirectoryItem(person) {&lt;br /&gt;
    var summary = truncateSummary((person[COL.summary] || &#039;&#039;).trim(), 300);&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      buildHeaderHTML(person) +&lt;br /&gt;
      (summary ? &#039;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot;&amp;gt;&#039; + esc(summary) + &#039;&amp;lt;/p&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      buildLinksHTML(person, &#039;links&#039;);&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeAccordionItem(person, idx) {&lt;br /&gt;
    var summary = truncateSummary((person[COL.summary] || &#039;&#039;).trim(), 300);&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
&lt;br /&gt;
    var panelId = &#039;ohr-panel-&#039; + idx;&lt;br /&gt;
    var btnId = &#039;ohr-btn-&#039; + idx;&lt;br /&gt;
&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-head&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-titleblock&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
          (function () {&lt;br /&gt;
            var name = fullName(person);&lt;br /&gt;
            var life = lifespan(person);&lt;br /&gt;
            var rec = recordedLabel(person);&lt;br /&gt;
&lt;br /&gt;
            var metaBits = [];&lt;br /&gt;
            if (life) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(life) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
            if (rec) metaBits.push(&#039;&amp;lt;span&amp;gt;&#039; + esc(rec) + &#039;&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
            var meta = metaBits.join(&#039;&amp;lt;span class=&amp;quot;ohr-dot&amp;quot;&amp;gt;·&amp;lt;/span&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
            return (&lt;br /&gt;
              &#039;&amp;lt;h3 class=&amp;quot;ohr-name&amp;quot;&amp;gt;&#039; + esc(name) + &#039;&amp;lt;/h3&amp;gt;&#039; +&lt;br /&gt;
              (meta ? &#039;&amp;lt;div class=&amp;quot;ohr-meta&amp;quot;&amp;gt;&#039; + meta + &#039;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;)&lt;br /&gt;
            );&lt;br /&gt;
          })() +&lt;br /&gt;
        &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;button class=&amp;quot;ohr-acc-btn&amp;quot; id=&amp;quot;&#039; + btnId + &#039;&amp;quot; type=&amp;quot;button&amp;quot; aria-expanded=&amp;quot;false&amp;quot; aria-controls=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;Details&amp;lt;/button&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;div class=&amp;quot;ohr-acc-panel ohr-hidden&amp;quot; id=&amp;quot;&#039; + panelId + &#039;&amp;quot;&amp;gt;&#039; +&lt;br /&gt;
        (summary ? &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot; style=&amp;quot;margin:0&amp;quot;&amp;gt;&#039; + esc(summary) + &#039;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
        &#039;&amp;lt;div class=&amp;quot;ohr-divider&amp;quot;&amp;gt;&#039; + buildLinksHTML(person, &#039;links&#039;) + &#039;&amp;lt;/div&amp;gt;&#039; +&lt;br /&gt;
      &#039;&amp;lt;/div&amp;gt;&#039;;&lt;br /&gt;
&lt;br /&gt;
    var btn = div.querySelector(&#039;#&#039; + btnId);&lt;br /&gt;
    var panel = div.querySelector(&#039;#&#039; + panelId);&lt;br /&gt;
    if (btn &amp;amp;&amp;amp; panel) {&lt;br /&gt;
      btn.addEventListener(&#039;click&#039;, function () {&lt;br /&gt;
        var open = !panel.classList.contains(&#039;ohr-hidden&#039;);&lt;br /&gt;
        if (open) {&lt;br /&gt;
          panel.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);&lt;br /&gt;
          btn.textContent = &#039;Details&#039;;&lt;br /&gt;
        } else {&lt;br /&gt;
          panel.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
          btn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);&lt;br /&gt;
          btn.textContent = &#039;Hide&#039;;&lt;br /&gt;
        }&lt;br /&gt;
      });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function makeChipsItem(person) {&lt;br /&gt;
    var summary = truncateSummary((person[COL.summary] || &#039;&#039;).trim(), 300);&lt;br /&gt;
    var div = document.createElement(&#039;div&#039;);&lt;br /&gt;
    div.className = &#039;ohr-item&#039;;&lt;br /&gt;
    div.innerHTML =&lt;br /&gt;
      buildHeaderHTML(person) +&lt;br /&gt;
      (summary ? &#039;&amp;lt;p class=&amp;quot;ohr-bio&amp;quot;&amp;gt;&#039; + esc(summary) + &#039;&amp;lt;/p&amp;gt;&#039; : &#039;&#039;) +&lt;br /&gt;
      buildLinksHTML(person, &#039;chips&#039;);&lt;br /&gt;
    return div;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function renderList(rows) {&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!container) return;&lt;br /&gt;
&lt;br /&gt;
    if (!rows.length) {&lt;br /&gt;
      container.innerHTML = &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;text-align:center&amp;quot;&amp;gt;No matching records.&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    var mode = getViewMode();&lt;br /&gt;
    var frag = document.createDocumentFragment();&lt;br /&gt;
&lt;br /&gt;
    for (var i = 0; i &amp;lt; rows.length; i++) {&lt;br /&gt;
      if (mode === &#039;accordion&#039;) frag.appendChild(makeAccordionItem(rows[i], i));&lt;br /&gt;
      else if (mode === &#039;chips&#039;) frag.appendChild(makeChipsItem(rows[i]));&lt;br /&gt;
      else frag.appendChild(makeDirectoryItem(rows[i]));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    container.innerHTML = &#039;&#039;;&lt;br /&gt;
    container.appendChild(frag);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updateCount(n, total) {&lt;br /&gt;
    var el = $(&#039;ohr-count&#039;);&lt;br /&gt;
    if (!el) return;&lt;br /&gt;
    el.textContent = (n === total) ? (total + &#039; records&#039;) : (n + &#039; of &#039; + total + &#039; records&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Filtering ======&lt;br /&gt;
  function applyFilters() {&lt;br /&gt;
    var q = norm(FILTER.q);&lt;br /&gt;
&lt;br /&gt;
    // First remove invalid-name records entirely&lt;br /&gt;
    var valid = ALL.filter(function (row) {&lt;br /&gt;
      return hasValidName(row);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    var filtered = valid.filter(function (row) {&lt;br /&gt;
      if (!q) return true;&lt;br /&gt;
      var hay = norm(&lt;br /&gt;
        (row[COL.first] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.last] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.summary] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.birth] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.death] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.dateFrom] || &#039;&#039;) + &#039; &#039; +&lt;br /&gt;
        (row[COL.dateTo] || &#039;&#039;)&lt;br /&gt;
      );&lt;br /&gt;
      return hay.indexOf(q) !== -1;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    filtered.sort(function (a, b) {&lt;br /&gt;
      var A = norm((a[COL.last] || &#039;&#039;) + &#039; &#039; + (a[COL.first] || &#039;&#039;));&lt;br /&gt;
      var B = norm((b[COL.last] || &#039;&#039;) + &#039; &#039; + (b[COL.first] || &#039;&#039;));&lt;br /&gt;
      return A.localeCompare(B);&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    renderList(filtered);&lt;br /&gt;
    updateCount(filtered.length, valid.length);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function attachHandlers() {&lt;br /&gt;
    var qInput = $(&#039;ohr-search&#039;);&lt;br /&gt;
    if (qInput) {&lt;br /&gt;
      qInput.addEventListener(&#039;input&#039;, debounce(function () {&lt;br /&gt;
        FILTER.q = qInput.value.trim();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      }, 200));&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Errors ======&lt;br /&gt;
  function displayError(message) {&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var error = $(&#039;ohr-error&#039;);&lt;br /&gt;
    if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
    if (error) {&lt;br /&gt;
      error.classList.remove(&#039;ohr-hidden&#039;);&lt;br /&gt;
      error.classList.add(&#039;ohr-error&#039;);&lt;br /&gt;
      error.innerHTML =&lt;br /&gt;
        &#039;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Failed to load records.&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&#039; +&lt;br /&gt;
        &#039;&amp;lt;p class=&amp;quot;ohr-muted&amp;quot; style=&amp;quot;margin-top:0.5rem&amp;quot;&amp;gt;&#039; + esc(message) + &#039;&amp;lt;/p&amp;gt;&#039;;&lt;br /&gt;
    }&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ====== Main ======&lt;br /&gt;
  function fetchAndRender() {&lt;br /&gt;
    var root = $(&#039;ohr-directory&#039;);&lt;br /&gt;
    var loading = $(&#039;ohr-loading&#039;);&lt;br /&gt;
    var container = $(&#039;ohr-list&#039;);&lt;br /&gt;
    if (!root || !container) return;&lt;br /&gt;
&lt;br /&gt;
    buildToolbar(root);&lt;br /&gt;
&lt;br /&gt;
    fetch(getCsvUrl(), { credentials: &#039;include&#039;, cache: &#039;no-store&#039; })&lt;br /&gt;
      .then(function (res) {&lt;br /&gt;
        if (!res.ok) throw new Error(&#039;HTTP &#039; + res.status);&lt;br /&gt;
        return res.text();&lt;br /&gt;
      })&lt;br /&gt;
      .then(function (text) {&lt;br /&gt;
        ALL = parseCSV(text);&lt;br /&gt;
        if (loading) loading.classList.add(&#039;ohr-hidden&#039;);&lt;br /&gt;
        attachHandlers();&lt;br /&gt;
        applyFilters();&lt;br /&gt;
      })&lt;br /&gt;
      .catch(function (err) {&lt;br /&gt;
        displayError(&#039;There was an error accessing the records. Details: &#039; + err.message);&lt;br /&gt;
        console.error(err);&lt;br /&gt;
      });&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  onReady(function () {&lt;br /&gt;
    if ($(&#039;ohr-directory&#039;)) {&lt;br /&gt;
      mw.loader.using([]).then(fetchAndRender);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
})();&lt;/div&gt;</summary>
		<author><name>Hthach</name></author>
	</entry>
</feed>