1/* ============================================================ 2 ImpactMojo Global Search â Ctrl+K / Cmd+K 3 Uses Fuse.js for fuzzy matching across all content 4 ============================================================ */ 5(function () { 6 'use strict'; 7 8 var FUSE_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.min.js'; 9 var INDEX_URL = '/data/search-index.json'; 10 11 var fuse = null; 12 var searchData = null; 13 var modal = null; 14 var input = null; 15 var resultsList = null; 16 var selectedIndex = -1; 17 var visible = false; 18 19 var SARGAM_CDN = 'https://cdn.jsdelivr.net/npm/[email protected]/Icons/Line/'; 20 function sargamImg(name) { 21 return '<img src="' + SARGAM_CDN + name + '.svg" alt="" style="width:18px;height:18px;filter:brightness(0) invert(1);" loading="lazy">'; 22 } 23 24 var TYPE_META = { 25 bct: { icon: sargamImg('si_Fact_check'), label: 'BCT Technique', color: '#EC4899' }, 26 dataverse: { icon: sargamImg('si_Bar_chart'), label: 'Dataverse', color: '#6366F1' }, 27 course: { icon: sargamImg('si_Book'), label: 'Course', color: '#0EA5E9' }, 28 game: { icon: sargamImg('si_Activity'), label: 'Game', color: '#F59E0B' }, 29 lab: { icon: sargamImg('si_Wrench'), label: 'Studio', color: '#10B981' }, 30 page: { icon: sargamImg('si_Article'), label: 'Page', color: '#8B5CF6' }, 31 'book-summary': { icon: sargamImg('si_Book'), label: 'Reading Companion', color: '#D97706' }, 32 blog: { icon: sargamImg('si_Article'), label: 'Blog Post', color: '#0891B2' }, 33 'deep-dive': { icon: sargamImg('si_Library_books'), label: 'Deep Dive', color: '#7C3AED' }, 34 product: { icon: sargamImg('si_Bag'), label: 'Product', color: '#DB2777' }, 35 'practice-pack': { icon: sargamImg('si_Fact_check'), label: 'Practice Pack', color: '#059669' }, 36 tool: { icon: sargamImg('si_Wrench'), label: 'Tool', color: '#2563EB' }, 37 'data-dive': { icon: sargamImg('si_Bar_chart'), label: 'Data Note', color: '#4F46E5' }, 38 timeline: { icon: sargamImg('si_Timer'), label: 'Timeline', color: '#B45309' }, 39 poster: { icon: sargamImg('si_Image'), label: 'Poster', color: '#BE185D' }, 40 reference: { icon: sargamImg('si_Library_books'), label: 'Reference', color: '#0D9488' }, 41 handout: { icon: sargamImg('si_File_download'), label: 'Handout', color: '#65A30D' }, 42 challenge: { icon: sargamImg('si_Flag'), label: 'Challenge', color: '#DC2626' }, 43 showcase: { icon: sargamImg('si_Image'), label: 'Work Sample', color: '#7C2D12' }, 44 special: { icon: sargamImg('si_Library_books'), label: 'Special', color: '#9333EA' }, 45 'field-radio': { icon: sargamImg('si_Mic'), label: 'Field Radio', color: '#0EA5E9' } 46 }; 47 48 /* ---- Load Fuse.js ---- */ 49 function loadFuse(cb) { 50 if (window.Fuse) return cb(); 51 var s = document.createElement('script'); 52 s.src = FUSE_CDN; 53 s.onload = cb; 54 s.onerror = function () { console.warn('ImpactMojo Search: Failed to load Fuse.js'); }; 55 document.head.appendChild(s); 56 } 57 58 /* ---- Load search index ---- */ 59 function loadIndex(cb) { 60 if (searchData) return cb(searchData); 61 var controller = window.AbortController ? new AbortController() : null; 62 var timeoutId = controller ? setTimeout(function () { controller.abort(); }, 10000) : null; 63 fetch(INDEX_URL, controller ? { signal: controller.signal } : {}) 64 .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }) 65 .then(function (data) { searchData = data; cb(data); }) 66 .catch(function (e) { console.warn('ImpactMojo Search: Failed to load index', e); }) 67 .finally(function () { if (timeoutId) clearTimeout(timeoutId); }); 68 } 69 70 /* ---- Initialize Fuse instance ---- */ 71 function initFuse() { 72 loadFuse(function () { 73 loadIndex(function (data) { 74 fuse = new window.Fuse(data, { 75 keys: [ 76 { name: 'title', weight: 0.45 }, 77 { name: 'description', weight: 0.25 }, 78 { name: 'tags', weight: 0.15 }, 79 { name: 'category', weight: 0.1 }, 80 { name: 'id', weight: 0.05 } 81 ], 82 threshold: 0.35, 83 distance: 200, 84 includeScore: true, 85 minMatchCharLength: 2, 86 limit: 20 87 }); 88 }); 89 }); 90 } 91 92 /* ---- Build modal HTML ---- */ 93 function createModal() { 94 modal = document.createElement('div'); 95 modal.id = 'impactmojo-search-modal'; 96 modal.setAttribute('role', 'dialog'); 97 modal.setAttribute('aria-label', 'Search ImpactMojo'); 98 modal.innerHTML = 99 '<div class="ims-backdrop"></div>' + 100 '<div class="ims-dialog">' + 101 '<div class="ims-input-row">' +
102 '<svg class="ims-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>' + 103 '<input id="ims-input" type="text" placeholder="Search courses, tools, techniques..." autocomplete="off" aria-label="Search">' + 104 '<kbd class="ims-kbd">ESC</kbd>' + 105 '</div>' + 106 '<div id="ims-results" class="ims-results"></div>' + 107 '<div class="ims-footer">' + 108 '<span><kbd>â</kbd><kbd>â</kbd> navigate</span>' + 109 '<span><kbd>âµ</kbd> open</span>' + 110 '<span><kbd>esc</kbd> close</span>' + 111 '<span class="ims-count" id="ims-count"></span>' + 112 '</div>' + 113 '</div>'; 114 document.body.appendChild(modal); 115 116 input = document.getElementById('ims-input'); 117 resultsList = document.getElementById('ims-results'); 118 119 modal.querySelector('.ims-backdrop').addEventListener('click', closeSearch); 120 input.addEventListener('input', onInput); 121 input.addEventListener('keydown', onKeydown); 122 } 123 124 /* ---- Inject CSS ---- */ 125 function injectStyles() { 126 var css = document.createElement('style'); 127 css.textContent = 128 '#impactmojo-search-modal{display:none;position:fixed;inset:0;z-index:99999;font-family:"Amaranth","Inter",system-ui,sans-serif}' + 129 '#impactmojo-search-modal.ims-open{display:flex;align-items:flex-start;justify-content:center;padding-top:min(20vh,120px)}' + 130 '.ims-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px)}' + 131 '.ims-dialog{position:relative;width:min(640px,90vw);max-height:min(500px,70vh);background:var(--card-bg,#fff);border-radius:16px;box-shadow:0 25px 50px rgba(0,0,0,0.25);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-color,#e2e8f0)}' + 132 '.ims-input-row{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-color,#e2e8f0)}' + 133 '.ims-search-icon{width:20px;height:20px;flex-shrink:0;color:var(--text-muted,#94a3b8)}' + 134 '#ims-input{flex:1;border:none;outline:none;font-size:1rem;background:transparent;color:var(--text-primary,#1e293b);font-family:inherit}' + 135 '#ims-input::placeholder{color:var(--text-muted,#94a3b8)}' + 136 '.ims-kbd{font-size:0.7rem;padding:2px 6px;border-radius:4px;border:1px solid var(--border-color,#e2e8f0);color:var(--text-secondary,#475569);background:var(--surface-color,#f8fafc);font-family:monospace;flex-shrink:0}' + 137 '.ims-results{overflow-y:auto;flex:1;padding:8px}' + 138 '.ims-results::-webkit-scrollbar{width:6px}.ims-results::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}' + 139 '.ims-result{display:flex;align-items:flex-start;gap:12px;padding:10px 12px;border-radius:10px;cursor:pointer;text-decoration:none;color:inherit;transition:background 0.15s}' + 140 '.ims-result:hover,.ims-result.ims-active{background:var(--surface-color,#f1f5f9)}' + 141 '.ims-result-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0;color:#fff}' + 142 '.ims-result-body{flex:1;min-width:0}' + 143 '.ims-result-title{font-weight:600;font-size:0.9rem;color:var(--text-primary,#1e293b);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' + 144 '.ims-result-desc{font-size:0.78rem;color:var(--text-muted,#64748b);margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}' + 145 '.ims-result-meta{display:flex;gap:8px;margin-top:4px}' + 146 '.ims-result-badge{font-size:0.65rem;padding:1px 6px;border-radius:4px;font-weight:600;color:#fff}' + 147 '.ims-footer{display:flex;align-items:center;gap:16px;padding:10px 20px;border-top:1px solid var(--border-color,#e2e8f0);font-size:0.75rem;color:var(--text-muted,#94a3b8)}' + 148 '.ims-footer kbd{font-size:0.7rem;padding:1px 5px;border-radius:3px;border:1px solid var(--border-color,#e2e8f0);background:var(--surface-color,#f8fafc);font-family:monospace;margin:0 2px}' + 149 '.ims-count{margin-left:auto}' + 150 '.ims-empty{padding:40px 20px;text-align:center;color:var(--text-muted,#94a3b8)}
150' + 151 '.ims-empty-icon{font-size:2rem;margin-bottom:8px}' + 152 '.ims-empty-text{font-size:0.9rem}' + 153 '.ims-empty-hint{font-size:0.78rem;margin-top:4px}' + 154 /* Navbar search button */ 155 '.ims-nav-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;border:1px solid var(--border-color,#e2e8f0);background:var(--surface-color,#f8fafc);color:var(--text-secondary,#475569);cursor:pointer;font-size:0.82rem;font-family:inherit;transition:all 0.2s;margin-left:8px}' + 156 '.ims-nav-btn:hover{border-color:var(--accent-color,#6366F1);color:var(--text-primary,#1e293b);box-shadow:0 2px 8px rgba(99,102,241,0.15)}' + 157 '.ims-nav-btn svg{width:15px;height:15px}' + 158 '.ims-nav-btn kbd{font-size:0.65rem;padding:1px 5px;border-radius:3px;border:1px solid var(--border-color,#d1d5db);background:var(--card-bg,#fff);font-family:monospace;color:var(--text-secondary,#475569)}' + 159 '@media(max-width:768px){.ims-nav-btn kbd{display:none}.ims-nav-btn{padding:6px 10px}}' + 160 /* Mobile button (#1062). The desktop button lives in .nav-buttons, which 161 css/imx-main.css sets to display:none !important below 768px, so on a 162 phone it was in the DOM at 0x0 and there was no search entry in the 163 hamburger menu either. This one sits in .nav-container, which stays 164 visible, and appears only where the other one disappears. 44px square 165 is the minimum comfortable tap target. */ 166 /* order:98 because the mobile bar sequences itself with flex order, not 167 DOM order: .logo-container is 1, .nav-buttons 2, .mobile-menu-toggle 168 99. A button left at the default 0 renders before the logo, which is 169 where this one first landed. 98 puts it immediately left of the 170 hamburger, and margin-left:auto keeps the pair together on the right. */ 171 '.ims-nav-btn--m{display:none;align-items:center;justify-content:center;width:44px;height:44px;padding:0;' + 172 'order:98;margin:0 4px 0 auto;border-radius:10px;border:1px solid var(--border-color,#e2e8f0);' + 173 'background:var(--surface-color,#f8fafc);color:var(--text-secondary,#475569);cursor:pointer;flex:0 0 auto}' + 174 '.ims-nav-btn--m svg{width:20px;height:20px}' + 175 '[data-theme="dark"] .ims-nav-btn--m{background:#1e293b;color:#e2e8f0;border-color:#334155}' + 176 '@media(max-width:768px){.ims-nav-btn--m{display:inline-flex}}' + 177 /* --text-secondary is defined in dark mode but --surface-color is not, 178 so the button kept its light fallback background under light text: 179 1.42:1. Pin both sides for dark rather than relying on the vars. */ 180 '[data-theme="dark"] .ims-nav-btn{background:#1e293b;color:#e2e8f0;border-color:#334155}'; 181 document.head.appendChild(css); 182 } 183 184 /* ---- Field Radio transcripts ---- 185 The station's 19 clips carry ~45,000 characters of transcript. They are 186 deliberately NOT in data/search-index.json: that file is fetched on every 187 page load (initFuse runs on DOMContentLoaded below), so carrying them 188 there would charge every visitor on every page for a search few of them 189 run. They are fetched here instead, once, on the first query. 190 191 Why a second Fuse instance rather than another key on the main one: the 192 main index uses Fuse's default location scoring with distance 200, which 193 ranks a match by how near it sits to the start of the field. That is fine 194 for a title and useless for a 4,000-character transcript -- measured, a 195 transcript key on the main index matched nothing that was not in the 196 opening line. ignoreLocation fixes it, and setting it globally moved the 197 top result on a quarter of ordinary queries. So it is set here only. */ 198 var trFuse = null, trTried = false; 199 200 function loadTranscripts() { 201 if (trTried) return; 202 trTried = true; 203 fetch('/data/field-radio.json') 204 .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }) 205 .then(function (d) { 206 var clips = (d && Array.isArray(d.clips) ? d.clips : []).filter(function (c) { 207 return c && c.id && c.transcript; 208 }); 209 if (clips.length && window.Fuse) { 210 trFuse = new window.Fuse(clips, { 211 keys: [{ name: 'transcript' }], 212 threshold: 0.3, 213 ignoreLocation: true, 214 minMatchCharLength: 4, 215 includeScore: true, 216 limit: 5 217 }); 218 } 219 }) 220 .catch(function (e) { console.warn('ImpactMojo Search: transcripts unavailable', e); }); 221 } 222 223 /* Clips whose transcript matches, as index entries, minus any already shown. */ 224 function transcriptHits(query, already) { 225 if (!trFuse || query.length < 4) return []; 226 var seen = {}; 227 already.forEach(function (r) { seen[r.item.id] = true; }); 228 var out = [];
229 trFuse.search(query).forEach(function (r) { 230 var entry = null; 231 for (var i = 0; i < searchData.length; i++) { 232 if (searchData[i].id === 'FIELD-RADIO-' + r.item.id) { entry = searchData[i]; break; } 233 } 234 if (entry && !seen[entry.id]) { seen[entry.id] = true; out.push({ item: entry }); } 235 }); 236 return out; 237 } 238 239 /* ---- Render results ---- */ 240 function renderResults(query) { 241 if (!fuse || !query || query.length < 2) { 242 resultsList.innerHTML = 243 '<div class="ims-empty">' + 244 '<div class="ims-empty-icon">ð</div>' + 245 '<div class="ims-empty-text">Search ' + (searchData ? searchData.length : 'all') + ' resources across ImpactMojo</div>' + 246 '<div class="ims-empty-hint">Courses, BCT techniques, datasets, games, studios, and more</div>' + 247 '</div>'; 248 document.getElementById('ims-count').textContent = ''; 249 selectedIndex = -1; 250 return; 251 } 252 253 var results = fuse.search(query, { limit: 15 }); 254 // Clips whose spoken words match go after the title/description matches: 255 // a phrase someone half-remembers from a voice note should find it, but 256 // should not outrank a course whose title is what they asked for. 257 results = results.concat(transcriptHits(query, results)); 258 if (!results.length) { 259 resultsList.innerHTML = 260 '<div class="ims-empty">' + 261 '<div class="ims-empty-icon">ð¤</div>' + 262 '<div class="ims-empty-text">No results for "' + escapeHTML(query) + '"</div>' + 263 '<div class="ims-empty-hint">Try broader terms like "health", "monitoring", or "economics"</div>' + 264 '</div>'; 265 document.getElementById('ims-count').textContent = ''; 266 selectedIndex = -1; 267 return; 268 } 269 270 var html = ''; 271 results.forEach(function (r, i) { 272 var item = r.item; 273 var meta = TYPE_META[item.type] || { icon: sargamImg('si_Article'), label: item.type, color: '#64748b' }; 274 html += 275 '<a class="ims-result' + (i === 0 ? ' ims-active' : '') + '" href="' + escapeHTML(item.url) + '" data-idx="' + i + '"' + 276 (item.url.indexOf('http') === 0 ? ' target="_blank" rel="noopener noreferrer"' : '') + '>' + 277 '<div class="ims-result-icon" style="background:' + meta.color + '">' + meta.icon + '</div>' + 278 '<div class="ims-result-body">' + 279 '<div class="ims-result-title">' + highlightMatch(item.title, query) + '</div>' + 280 '<div class="ims-result-desc">' + escapeHTML(item.description) + '</div>' + 281 '<div class="ims-result-meta">' + 282 '<span class="ims-result-badge" style="background:' + meta.color + '">' + meta.label + '</span>' + 283 (item.category ? '<span class="ims-result-badge" style="background:#64748b">' + escapeHTML(item.category) + '</span>' : '') + 284 '</div>' + 285 '</div>' + 286 '</a>'; 287 }); 288 resultsList.innerHTML = html; 289 selectedIndex = 0; 290 document.getElementById('ims-count').textContent = results.length + ' result' + (results.length !== 1 ? 's' : ''); 291 292 resultsList.querySelectorAll('.ims-result').forEach(function (el) { 293 el.addEventListener('mouseenter', function () { 294 setActive(parseInt(el.dataset.idx)); 295 }); 296 }); 297 } 298 299 function highlightMatch(text, query) { 300 var safe = escapeHTML(text); 301 var q = escapeHTML(query); 302 var regex = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi'); 303 return safe.replace(regex, '<strong style="color:var(--accent-color,#6366F1)">$1</strong>'); 304 } 305 306 function escapeHTML(str) { 307 var map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; 308 return String(str || '').replace(/[&<>"']/g, function (c) { return map[c]; }); 309 } 310 311 /* ---- Keyboard navigation ---- */ 312 function setActive(idx) { 313 var items = resultsList.querySelectorAll('.ims-result'); 314 if (!items.length) return; 315 items.forEach(function (el) { el.classList.remove('ims-active'); }); 316 selectedIndex = Math.max(0, Math.min(idx, items.length - 1)); 317 items[selectedIndex].classList.add('ims-active'); 318 items[selectedIndex].scrollIntoView({ block: 'nearest' }); 319 } 320 321 function onKeydown(e) { 322 var items = resultsList.querySelectorAll('.ims-result'); 323 if (e.key === 'ArrowDown') { 324 e.preventDefault();
325 setActive(selectedIndex + 1); 326 } else if (e.key === 'ArrowUp') { 327 e.preventDefault(); 328 setActive(selectedIndex - 1); 329 } else if (e.key === 'Enter' && items[selectedIndex]) { 330 e.preventDefault(); 331 items[selectedIndex].click(); 332 closeSearch(); 333 } else if (e.key === 'Escape') { 334 e.preventDefault(); 335 closeSearch(); 336 } 337 } 338 339 function onInput() { 340 renderResults(input.value.trim()); 341 } 342 343 /* ---- Open / Close ---- */ 344 function openSearch() { 345 if (visible) return; 346 if (!modal) createModal(); 347 if (!fuse) initFuse(); 348 loadTranscripts(); // fetched when someone opens search, not on every page load 349 visible = true; 350 modal.classList.add('ims-open'); 351 input.value = ''; 352 renderResults(''); 353 setTimeout(function () { input.focus(); }, 50); 354 document.body.style.overflow = 'hidden'; 355 } 356 357 function closeSearch() { 358 if (!visible) return; 359 visible = false; 360 modal.classList.remove('ims-open'); 361 document.body.style.overflow = ''; 362 } 363 364 /* ---- Global keyboard shortcut ---- */ 365 document.addEventListener('keydown', function (e) { 366 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { 367 e.preventDefault(); 368 if (visible) closeSearch(); else openSearch(); 369 } 370 if (e.key === '/' && !visible && !isInputFocused()) { 371 e.preventDefault(); 372 openSearch(); 373 } 374 }); 375 376 function isInputFocused() { 377 var el = document.activeElement; 378 return el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable); 379 } 380 381 var ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' + 382 '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'; 383 384 function makeButton(cls, label, inner) { 385 var btn = document.createElement('button'); 386 btn.type = 'button'; 387 btn.className = cls; 388 btn.setAttribute('aria-label', label); 389 btn.innerHTML = inner; 390 btn.addEventListener('click', function (e) { e.preventDefault(); openSearch(); }); 391 return btn; 392 } 393 394 /* ---- Inject nav button ---- */ 395 function injectNavButton() { 396 /* Pages that load the shared top bar already have .im-sc-search there, and 397 that one keeps its icon on a narrow screen. Two search buttons in one bar 398 is worse than one. */ 399 if (document.querySelector('.im-sc-search')) return; 400 401 var btn = makeButton('ims-nav-btn', 'Search (Ctrl+K)', ICON + 'Search' + '<kbd>âK</kbd>'); 402 403 /* Place in nav-buttons area (next to theme selector / auth buttons) for prominence */ 404 var navButtons = document.querySelector('.nav-buttons'); 405 if (navButtons) { 406 var themeSelector = navButtons.querySelector('.theme-selector'); 407 if (themeSelector) { 408 navButtons.insertBefore(btn, themeSelector); 409 } else { 410 navButtons.insertBefore(btn, navButtons.firstChild); 411 } 412 injectMobileButton(); 413 return; 414 } 415 416 /* Fallback: append to nav container. This one is not inside .nav-buttons, 417 so it stays visible on a phone and needs no companion. */ 418 var nav = document.querySelector('.nav-with-badge') || document.querySelector('.nav-container'); 419 if (nav) { 420 nav.style.display = 'flex'; 421 nav.style.alignItems = 'center'; 422 nav.appendChild(btn); 423 } 424 } 425 426 /* ---- Mobile companion (#1062) ---- 427 .nav-buttons is display:none below 768px, so the button above cannot be 428 reached on a phone; the hamburger menu carries no search entry either, and 429 Ctrl+K is not available to someone holding a phone. This puts an icon-only 430 button in .nav-container, which stays laid out at every width, next to the 431 hamburger. CSS shows it only below the same breakpoint, so the two are 432 never both on screen. */ 433 function injectMobileButton() { 434 var container = document.querySelector('.nav-container'); 435 if (!container || container.querySelector('.ims-nav-btn--m')) return; 436 var btn = makeButton('ims-nav-btn ims-nav-btn--m', 'Search', ICON); 437 var toggle = container.querySelector('.mobile-menu-toggle, #mobile-menu-btn'); 438 if (toggle) container.insertBefore(btn, toggle); 439 else container.appendChild(btn); 440 } 441 442 /* ---- Init ---- */ 443 injectStyles(); 444 445 if (document.readyState === 'loading') { 446 document.addEventListener('DOMContentLoaded', function () { 447 injectNavButton(); 448 initFuse(); 449 }); 450 } else { 451 injectNavButton(); 452 initFuse(); 453 } 454 455 window.ImpactMojoSearch = { open: openSearch, close: closeSearch }; 456})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.