Seek — an inspectable search product
Build a complete original search-product website named Seek. Its central promise is that someone can understand how a result was found, change the explicit rules, and inspect the original document. This is an interactive browser demonstration over ten fictional records. It does not create an external index, contact a provider, correct spelling, infer meaning, or perform AI retrieval. Keep this boundary visible in the guide, footer, product descriptions, and settings. Do not add production benchmarks, customer logos, fabricated testimonials, signup forms, billing, or implied external integrations.
Reference and original direction
Study https://www.meilisearch.com/ and https://www.meilisearch.com/products/faceted-search as composition references. Their useful structural principle is immediate product evidence: a working search field appears beside actual results, and subsequent sections explain how filtering and ranking affect those results. On the live homepage, a regular keyword query changed movie results while the AI toggle remained off. The facets page progresses from a product demonstration to comparison, feature detail, applications, FAQ, and a closing action. Use this explanatory sequence, not their assets, movie dataset, purple identity, typography, wording, or source code. Seek instead uses original editorial records and a paper, charcoal, and lime visual identity. Every product panel is actual HTML connected to the same local engine as the dedicated playground.
The identity is lowercase seek with an original bracket-and-dot mark. Brackets suggest an addressable piece of information rather than a decorative magnifying glass. Use Geist for readable body and interface text, and Space Grotesk for headings and the wordmark. Serve licensed font files locally, retain their license text, and make the fonts usable without third-party requests. Use small Lucide icons only where they clarify search, export, keyboard access, or navigation. Preserve the icon license. All document copy is original fictional material. The hero thumbnail is an original graphic representation of this interface, not a copied screenshot from the reference.
Site structure and source contract
Produce six substantial routes: index.html, search.html, tuning.html, data.html, record.html, and guide.html. The record route accepts a stable id parameter and renders the complete corresponding document. It also preserves query and facet context in the return link. An unknown id must show a useful recovery state rather than crashing or rendering an undefined heading. Each page has one main landmark, one h1, a skip link, a consistent header, and a footer with working links to the other workspaces. The home page is the marketing narrative; the other routes should feel like purposeful tools rather than a repeated marketing template.
Keep generation in scripts/seek/build.mjs, exporting buildSeek() and returning an object with prompt. Keep the deterministic records and pure engine functions in data.mjs, reusable icon output in ui.mjs, the browser handlers in site.js, the shared visual rules in site.css, and this detailed recreation specification in PROMPT.md. The builder writes only public/demos/seek and public/prompts/seek.md. Assemble the real runtime JavaScript under the exact heading Reference interaction handler, including data and pure engine functions, so a future implementer can recreate behavior without guessing. Do not modify a shared catalog, application shell, deployment, or credentials.
Visual system and desktop geometry
Use ink #20261a, muted text #5d6655, paper #f8f9f0, border #ccd2bc, lime #d9ee7e, and pale green #e9efce. The overall feeling is a useful editorial instrument: flat sections, disciplined rules, large typography, and a small number of clearly differentiated product surfaces. Avoid excessive rounded cards, shadows on every section, gradients, stock people photographs, and ornamental charts. Keep the primary content rail broad with consistent horizontal gutters. Header height is approximately 78px. The left wordmark, central workspace navigation, and right query action align on a single baseline. Current navigation is conveyed through aria-current as well as visual treatment.
The homepage hero is a spacious two-column composition. The left half carries a small uppercase introduction, a very large three-line statement, two short lines of supporting copy, and one dark action. The right half is a real search window with a thin top bar, a visible label, a generous search input, three live results, and a bottom count and route link. The panel is not a raster mockup. Maintain legible result titles and summaries even while the headline is dramatic. The search field should be immediately recognizable and usable with a keyboard. Use an ordinary slash key hint as a convenience; never require it to use the interface.
After the hero, place a four-part feature strip on a single ruled line. It introduces keyword matching, explicit ranking, inspectable records, and keyboard access without claiming unsupported features. Follow with a centered or generously offset statement about context, then three equal explanation columns. Their visuals are native text constructions: query to result, Guide and Interface facet chips joined by AND, and title/summary/body weights. Each column is one real link to a relevant workspace. There must be no dead miniature controls inside those links.
The next section changes the full background to lime. On the left, a strong heading and paragraph explain explicit vocabulary; on the right, a large synonym diagram shows search, find, and lookup as alternatives within one group. Give this diagram breathing room and a readable limitations note. Then return to paper for a split document section: a believable, readable source excerpt on one side and explanatory typography on the other. Its source link opens the real empty-states document. Below, three large query-start links exercise exact context, synonyms, and prefixes. Finish with native disclosure FAQs and a large closing statement. The change in section scale and background provides rhythm without importing another site's brand.
Playground layout and states
The search route starts with a compact page introduction, then a full-width labeled query field. A short keyboard note explains slash, Escape, Tab, and Enter. Below it, use a fixed-width filter rail next to a fluid result list. The rail contains a native checkbox group for Guide, Reference, and Story; one topic select; a clear-all action; and a readable summary of active saved settings. The result column has a count header, current-query description, and vertically separated results. Each result includes type, topic, fixed sample date, title link, summary, and native details disclosure explaining its score. Do not represent a score as a confidence percentage.
Typing must recalculate results from the current query and active facets. Selected types combine with OR. The type group, topic, and query combine with AND. No selected type means all types. Counts beside type choices are calculated using the query and topic before applying the selected type group. This lets someone understand what another checkbox can contribute. The topic values are Search, Interface, Data, and Operations, plus Any topic. A zero-match combination shows a specific empty state and a working recovery button. Clearing the full view resets the query and both facet groups and returns focus to the query.
Keep a search term capped at 120 characters. Query punctuation is a separator, not an operator language. Render user text safely with escaping; highlighting may wrap matched original words in mark elements but must never interpret entered markup. Result detail URLs contain only the local route and encoded record/context parameters. Do not send queries over a network or store query history. The hero uses the same engine with only its first three results visible, while the playground displays the full eligible result set.
Exact matching and ranking contract
Normalize text with Unicode NFD, remove combining marks, lowercase it, and tokenize contiguous Unicode letters and numbers. Deduplicate normalized query words. Each query word becomes a group of alternatives when it belongs to a configured synonym group; otherwise it remains a group of one. Exact matching compares full normalized words. Enabled prefix matching also accepts indexed words beginning with an alternative. It does not reorder characters, perform edit-distance matching, stem words, or infer concepts. Make this distinction explicit in the guide and settings copy.
Search title, summary, and body paragraphs. In all-word mode, every query group must match somewhere in those fields. In any-word mode, at least one must match. An empty query browses the records permitted by facets. Eligibility is decided before ranking. For every matching group, award the configured title weight if it matches the title, the summary weight if it matches the summary, and one point if it matches the body. A group scores at most once in each field even if the word repeats. Add two points for an eligible record in the preferred topic, if one is configured. Sort total descending, optionally break ties by fixed sample updated date descending, then use stable record id. A preferred topic must never rescue a record that failed query or facets.
Defaults are title weight five, summary weight two, body weight one, all-word matching, prefix matching enabled, recent-date tie-breaking enabled, no preferred topic, and synonym groups search/find/lookup, export/download, and keyboard/shortcut. Preserve these concrete defaults in the form, preview, guide, exports, and reset behavior. The export-contract document and calmer-handoff story deliberately provide a useful ranking experiment: one matches export in its title and body, the other in its summary. Raising summary weight while lowering title weight changes their order without changing the underlying documents.
Tuning workspace and validation
Use a two-column tuning layout: the larger left column is the actual settings form, and the right column previews saved rules. Group controls by matching, ranking, and explicit vocabulary. Matching uses a styled select for all or any and a checkbox for prefixes. Ranking uses required integer number fields with title bounds one through eight and summary bounds one through five. Body is fixed at one and described in text. A topic preference select and recent-date checkbox follow. Keep all labels adjacent to their controls, and maintain enough vertical spacing that each setting can be scanned independently.
Synonyms use a multiline textarea with one comma-separated group per line. Ignore blank lines. Permit at most eight groups and two through six single-word terms in each group. Terms are limited to thirty characters. Reject multiword phrases, missing terms, duplicate normalized terms within or across groups, and malformed imported settings. Normalize valid terms before saving. An empty textarea means synonyms are disabled. Validation errors appear in a role-alert region, mark the textarea invalid where appropriate, and focus it so the error can be corrected. Do not partially apply a malformed configuration.
The preview is explicitly a saved-rule preview. Editing controls changes the unsaved status but does not secretly change the active engine. Saving first validates the entire form, then writes to the owned localStorage key seek-settings-v1. Only after a successful write does it apply the new configuration and refresh the preview and configuration JSON. If storage fails, retain previous applied settings and explain the failure. Invalid persisted data falls back to defaults. Restoring defaults resets only this product's owned configuration, not unrelated browser storage. Export validates the current form and may export a valid unsaved draft, with version and engine identifiers so the file's purpose is clear.
Dataset, detail, and guide
The dataset workspace contains a simple title/topic/type substring filter, a readable record list, expandable source JSON, and an export action for all ten fictional records. Explain that this source-browser filter is different from the weighted search playground. The schema table identifies stable id, title, summary, body, type, topic, and updated date. Full paragraphs remain present in exported data. Do not add an import button unless import is actually implemented. Do not call this an external index or imply that a provider has accepted the records.
The record page uses an editorial reading column and a smaller context rail. Show the original title, summary, metadata, and all body paragraphs with comfortable line length and vertical rhythm. The return link restores the original query and facet context. The guide uses a local section-navigation rail and substantial explanations of normalization, facet eligibility, synonym expansion, point calculation, keyboard flow, storage, and exports. Include a worked numerical example and explicit boundaries. These pages should remain useful independently of the homepage.
Responsive behavior and accessibility
At narrow widths, reflow product interfaces into normal document flow. The marketing hero becomes a single column, keeping both headline and usable search panel. Do not scale the desktop panel down. Three-column explanation and query sections become vertical sections with full-width readable links. The filter rail becomes an accessible group above results; it does not vanish behind an unimplemented drawer. The settings preview moves after the form, and the record and guide rails move above their content. Tables and JSON wrap or scroll within their own containers without widening the page.
Keep body text around sixteen to eighteen pixels, actual controls and content at least fourteen, and secondary metadata at least twelve. Use visible focus outlines, native controls, sufficient color contrast, and meaningful accessible names. Do not put a custom role on ordinary links just to imitate an application widget. Slash focuses the primary search field only when focus is outside an input, textarea, select, or editable region. Escape clears the focused primary query while retaining facets. Tab follows normal document order and Enter activates native links or controls. A skip link becomes visible on focus.
Motion should reinforce hierarchy without withholding content. Use short opacity and vertical entrance transitions through IntersectionObserver only when reduced motion is not requested. All sections are visible in the initial document, so a failed observer does not hide the page. Hover transitions affect color, border, or a small arrow translation, never layout geometry. Disable nonessential animation and smooth scrolling under prefers-reduced-motion. No autoplay carousel, artificial loading delay, fake typing, or continuously shifting result order is necessary.
Verification and acceptance
Test the pure engine with real fixture expectations: combined facets, exact and prefix differences, all versus any query groups, accent normalization, synonym expansion, changed field weights, noninflating repetition, topic preference eligibility, deterministic ties, and empty-query browsing. Test invalid synonym groups and malformed persisted settings. In the browser, type a query, combine facets, open score details, follow a record, return with context, edit and save rules, reject an invalid group, export JSON, and restore defaults. Test slash and Escape without interfering with another field. Check unknown record ids and zero-result recovery.
Inspect all six pages at desktop and at a 390-pixel viewport. Verify one h1, no horizontal document overflow, readable controls, fully visible focus, and useful reflow. Capture a representative homepage and tool state visually; DOM counts alone do not prove composition. Run scoped lint, syntax checks, and the deterministic validation script. Use Lighthouse as a supporting accessibility/performance check, not as a substitute for visual or interaction testing. Report source references separately from original implementation and distinguish observed source behavior from design inferences.
Reference interaction handler
const skTypes=["Guide","Reference","Story"];const skTopics=["Search","Interface","Data","Operations"];const skRecords=[{"id":"useful-empty-states","title":"Designing useful empty states","type":"Guide","topic":"Interface","updated":"2026-08-22","summary":"Turn missing content into a clear next action without inventing success.","body":["An empty state is a moment of orientation. Explain what belongs in the space and what someone can do next. A search with no matches should say so plainly instead of filling the screen with unrelated records.","Keep the action specific. Clear the current filters, change the query, or create the first item when creation is part of the product. Do not make an empty state look like an error if nothing has failed.","Preserve context as the person moves forward. A useful result screen makes the selected filters visible and lets someone remove them without starting again."]},{"id":"search-vocabulary","title":"Search vocabulary that meets people halfway","type":"Guide","topic":"Search","updated":"2026-08-28","summary":"Make explicit synonym groups for the words people use to find the same thing.","body":["People rarely share a perfect vocabulary with a product. A small explicit synonym group can connect search, find, and lookup without pretending to understand every possible meaning.","Keep groups specific and review them against real content. Broad associations can make unrelated records appear relevant. In this fictional library the groups are simple comma-separated single words.","A synonym expands a query term into alternatives. It does not add new facts to a document or rewrite the original record. Keep the rule visible so a surprising match can be explained."]},{"id":"export-contract","title":"Keeping export formats predictable","type":"Reference","topic":"Data","updated":"2026-07-19","summary":"Define a stable data contract for a portable document.","body":["An export should reflect the current selected records and the settings used to retrieve them. Keep the format version and field names explicit so another reader can inspect the file.","Use a consistent encoding and a safe filename. A JSON document is useful for structured data; a plain text document is useful for reading. Neither format should imply that something was uploaded or published.","Include the relevant limitations. A local fixture is not live production data, and a weighted keyword score is not a probability of correctness."]},{"id":"incident-chronology","title":"A practical incident chronology","type":"Guide","topic":"Operations","updated":"2026-08-11","summary":"Separate observed events, response actions, and open questions.","body":["A useful incident timeline states what was observed before it states what someone thinks caused it. Keep timestamps and the source of each event visible.","Acknowledging ownership is a response action. It does not prove that a service recovered. Record recovery separately and preserve the earlier failure history.","Write the next useful step in plain language. Internal investigation details and public updates may have different audiences, but both should stay faithful to the evidence."]},{"id":"small-screen-results","title":"Readable results on small screens","type":"Guide","topic":"Interface","updated":"2026-08-25","summary":"Reflow search controls and record details instead of shrinking the desktop.","body":["A narrow screen changes the arrangement, not the need to read. Keep the query visible, move facets into a useful normal-flow group, and let result descriptions wrap.","Use real links for record destinations and make keyboard focus easy to see. Search results do not need to mimic a command palette if a straightforward list works better.","Metadata should help selection. A topic, record type, and clear summary usually matter more than a tiny decorative timestamp or a row of unexplained icons."]},{"id":"ranking-fields","title":"Field weights and explainable ranking","type":"Reference","topic":"Search","updated":"2026-08-30","summary":"Understand how title, summary, and body matches contribute to a local score.","body":["A weighted keyword ranking rule can be simple enough to explain. Count whether a query group matches each field, then multiply by that field’s configured weight.","This example counts a group at most once per field. Repeating a word many times in one paragraph does not create an unlimited score advantage.","Facets decide eligibility before ranking. A topic boost can reorder eligible records but should never make a nonmatching record pass the keyword requirement."]},{"id":"calmer-handoff","title":"Fieldnotes from a calmer handoff","type":"Story","topic":"Operations","updated":"2026-08-02","summary":"Export the context, export the open questions, and keep the next step visible.","body":["The fictional team had a useful document but an unclear ending. Each person remembered a different decision, and the next owner needed to reconstruct the work.","They began writing a short handoff that named the goal, current state, open questions, and next action. The document did not solve the problem by itself, but it made the conversation easier to begin.","A good handoff can travel without losing its context. Keep the source material available and avoid presenting unfinished work as a completed result."]},{"id":"keyboard-path","title":"A keyboard path through a search page","type":"Guide","topic":"Interface","updated":"2026-08-17","summary":"Make query focus, filters, result links, and recovery work without a pointer.","body":["A visible label and a normal search field are a strong foundation. A shortcut can help, but it should not override typing inside another input or text area.","Use Tab to move through controls and Enter to follow a result link. Native checkboxes and selects already support predictable keyboard interaction.","Escape can clear a query when the query field has focus. Keep the active facets unless the person explicitly chooses to clear the whole view."]},{"id":"dataset-shape","title":"The shape of a searchable record","type":"Reference","topic":"Data","updated":"2026-07-28","summary":"Give each document a stable identifier, readable fields, and explicit facets.","body":["The fictional record schema has an identifier, title, summary, body paragraphs, type, topic, and an updated date. The searchable fields are title, summary, and body.","Type and topic are exact facets. They are not inferred from a query and are not rewritten by the ranking settings.","Keep the original record readable. A result should lead to a detail page with the full content, not a dead preview or an unexplained identifier."]},{"id":"prefix-boundary","title":"Where prefix matching stops","type":"Reference","topic":"Search","updated":"2026-08-09","summary":"A partial beginning can match a word without becoming typo tolerance.","body":["Prefix matching compares the beginning of a normalized word. A query such as rank can match ranking when the option is enabled.","It does not repair misspellings, reorder characters, or understand a related concept. Those behaviors require different rules and should not be claimed by a simple prefix matcher.","A clear boundary helps people tune the experience. Turn prefix matching off to compare exact word matching using the same records and facets."]}];const skDefaults={"titleWeight":5,"summaryWeight":2,"match":"all","prefix":true,"recent":true,"preferredTopic":"Any topic","synonyms":[["search","find","lookup"],["export","download"],["keyboard","shortcut"]]};
function skTokens(text){return String(text).normalize('NFD').replace(/\p{M}/gu,'').toLowerCase().match(/[\p{L}\p{N}]+/gu)||[];}
function skBuildIndex(records){return records.map(record=>({record,fields:{title:skTokens(record.title),summary:skTokens(record.summary),body:skTokens(record.body.join(' '))}}));}
function skParseSynonyms(text){const lines=text.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);if(lines.length>8)return {ok:false,error:'Use at most 8 synonym groups.'};const seen=new Set();const groups=[];for(const line of lines){const parts=line.split(',').map(s=>s.trim());if(parts.length<2||parts.length>6)return {ok:false,error:'Each group needs 2 to 6 comma-separated single words.'};const group=[];for(const part of parts){const tokens=skTokens(part);if(tokens.length!==1||part.length>30)return {ok:false,error:'Use one word per term, up to 30 characters. Phrases are not supported.'};const word=tokens[0];if(seen.has(word))return {ok:false,error:'Each word can appear only once across all synonym groups.'};seen.add(word);group.push(word);}groups.push(group);}return {ok:true,groups};}
function skValidSettings(s){if(!s||typeof s!=='object')return false;const parsed=Array.isArray(s.synonyms)&&s.synonyms.every(g=>Array.isArray(g))?skParseSynonyms(s.synonyms.map(g=>g.join(',')).join('\n')):{ok:false};return Number.isInteger(s.titleWeight)&&s.titleWeight>=1&&s.titleWeight<=8&&Number.isInteger(s.summaryWeight)&&s.summaryWeight>=1&&s.summaryWeight<=5&&['all','any'].includes(s.match)&&typeof s.prefix==='boolean'&&typeof s.recent==='boolean'&&['Any topic',...skTopics].includes(s.preferredTopic)&&parsed.ok&&JSON.stringify(parsed.groups)===JSON.stringify(s.synonyms);}
function skQueryGroups(query,settings){return [...new Set(skTokens(query))].map(token=>settings.synonyms.find(g=>g.includes(token))||[token]);}
function skFieldMatches(words,group,prefix){return group.some(term=>words.some(word=>word===term||(prefix&&word.startsWith(term))));}
function skScore(entry,groups,settings){const matched=groups.filter(group=>Object.values(entry.fields).some(words=>skFieldMatches(words,group,settings.prefix))).length;if(groups.length&&(settings.match==='all'?matched!==groups.length:matched===0))return null;const points={title:groups.filter(g=>skFieldMatches(entry.fields.title,g,settings.prefix)).length*settings.titleWeight,summary:groups.filter(g=>skFieldMatches(entry.fields.summary,g,settings.prefix)).length*settings.summaryWeight,body:groups.filter(g=>skFieldMatches(entry.fields.body,g,settings.prefix)).length,topic:entry.record.topic===settings.preferredTopic?2:0};return {record:entry.record,score:Object.values(points).reduce((a,b)=>a+b,0),points,matched};}
function skSearch(index,query,facets,settings){const groups=skQueryGroups(query,settings);return index.filter(e=>(!facets.types.length||facets.types.includes(e.record.type))&&(facets.topic==='Any topic'||e.record.topic===facets.topic)).map(e=>skScore(e,groups,settings)).filter(Boolean).sort((a,b)=>b.score-a.score||(settings.recent?b.record.updated.localeCompare(a.record.updated):0)||a.record.id.localeCompare(b.record.id));}
const skReduced=matchMedia('(prefers-reduced-motion: reduce)');
const skStore={read(){try{return JSON.parse(localStorage.getItem('seek-settings-v1')||'null');}catch{return null;}},write(value){try{localStorage.setItem('seek-settings-v1',JSON.stringify(value));return true;}catch{return false;}}};
const skEsc=text=>String(text).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const skStored=skStore.read();let skSettings=skValidSettings(skStored)?skStored:structuredClone(skDefaults);const skIndex=skBuildIndex(skRecords);
function skHighlight(text,groups){return text.split(/([\p{L}\p{N}]+)/u).map(part=>{const words=skTokens(part);return words.length&&groups.some(g=>skFieldMatches(words,g,skSettings.prefix))?'<mark>'+skEsc(part)+'</mark>':skEsc(part);}).join('');}
function skRecordUrl(id,query='',facets={types:[],topic:'Any topic'}){return 'record.html?'+new URLSearchParams({id,q:query,types:facets.types.join(','),topic:facets.topic}).toString();}
function skResultMarkup(hit,query,facets,compact=false){const r=hit.record;const groups=skQueryGroups(query,skSettings);return `<article class="search-result ${compact?'compact-result':''}"><div class="result-meta"><span>${r.type}</span><span>${r.topic}</span><time>${r.updated}</time></div><h3><a class="result-title" href="${skRecordUrl(r.id,query,facets)}">${skHighlight(r.title,groups)} <span aria-hidden="true">↗</span></a></h3><p>${skHighlight(r.summary,groups)}</p>${compact?'':`<details class="result-explanation"><summary>Why this result? <span>Score ${hit.score}</span></summary><p>Title ${hit.points.title} + summary ${hit.points.summary} + body ${hit.points.body} + topic ${hit.points.topic} = ${hit.score}. ${hit.matched} query group${hit.matched===1?'':'s'} matched. Scores are rule points, not confidence.</p></details>`}</article>`;}
function skRenderHero(){const input=document.getElementById('sk-hero-query');if(!input)return;const hits=skSearch(skIndex,input.value,{types:[],topic:'Any topic'},skSettings);document.getElementById('sk-hero-results').innerHTML=hits.slice(0,3).map(h=>skResultMarkup(h,input.value,{types:[],topic:'Any topic'},true)).join('')||'<p class="hero-no-match">No local match. Try search, export, or keyboard.</p>';document.getElementById('sk-hero-count').textContent=hits.length+' matching records · local keyword rules';}
if(document.getElementById('sk-hero-query')){document.getElementById('sk-hero-query').addEventListener('input',skRenderHero);skRenderHero();}
function skFacets(){return {types:[...document.querySelectorAll('[data-sk-type]:checked')].map(e=>e.dataset.skType),topic:document.getElementById('sk-topic').value};}
function skRenderSearch(){const query=document.getElementById('sk-query').value;const facets=skFacets();const hits=skSearch(skIndex,query,facets,skSettings);document.getElementById('sk-result-count').textContent=hits.length+(hits.length===1?' result':' results');document.getElementById('sk-query-description').textContent=query.trim()?'Matching '+(skSettings.match==='all'?'all':'any')+' query groups':'Browsing eligible records';document.getElementById('sk-results').innerHTML=hits.map(hit=>skResultMarkup(hit,query,facets)).join('');document.getElementById('sk-no-results').hidden=hits.length>0;document.getElementById('sk-active-settings').textContent='Title ×'+skSettings.titleWeight+' · Summary ×'+skSettings.summaryWeight+' · Body ×1. '+(skSettings.prefix?'Prefixes enabled.':'Exact words only.')+' '+skSettings.synonyms.length+' synonym groups.';const withoutTypes=skSearch(skIndex,query,{types:[],topic:facets.topic},skSettings);document.querySelectorAll('[data-sk-type-count]').forEach(e=>e.textContent=withoutTypes.filter(h=>h.record.type===e.dataset.skTypeCount).length);}
function skClearSearch(){document.getElementById('sk-query').value='';document.getElementById('sk-topic').value='Any topic';document.querySelectorAll('[data-sk-type]').forEach(e=>e.checked=false);skRenderSearch();document.getElementById('sk-query').focus();}
if(document.getElementById('sk-query')){const params=new URLSearchParams(location.search);document.getElementById('sk-query').value=(params.get('q')||'').slice(0,120);if(skTopics.includes(params.get('topic')))document.getElementById('sk-topic').value=params.get('topic');const types=(params.get('types')||'').split(',');document.querySelectorAll('[data-sk-type]').forEach(input=>{input.checked=types.includes(input.dataset.skType);input.addEventListener('change',skRenderSearch);});document.getElementById('sk-query').addEventListener('input',skRenderSearch);document.getElementById('sk-topic').addEventListener('change',skRenderSearch);document.getElementById('sk-clear').addEventListener('click',skClearSearch);document.getElementById('sk-recover-search').addEventListener('click',skClearSearch);skRenderSearch();}
function skDownload(value,name){const url=URL.createObjectURL(new Blob([JSON.stringify(value,null,2)+'\n'],{type:'application/json;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}
function skPopulateSettings(){document.getElementById('sk-match').value=skSettings.match;document.getElementById('sk-prefix').checked=skSettings.prefix;document.getElementById('sk-title-weight').value=skSettings.titleWeight;document.getElementById('sk-summary-weight').value=skSettings.summaryWeight;document.getElementById('sk-preferred-topic').value=skSettings.preferredTopic;document.getElementById('sk-recent').checked=skSettings.recent;document.getElementById('sk-synonyms').value=skSettings.synonyms.map(g=>g.join(', ')).join('\n');document.getElementById('sk-config-json').textContent=JSON.stringify(skSettings,null,2);}
function skReadSettingsForm(){const form=document.getElementById('sk-settings-form');if(!form.reportValidity())return null;const parsed=skParseSynonyms(document.getElementById('sk-synonyms').value);const error=document.getElementById('sk-config-error');if(!parsed.ok){error.textContent=parsed.error;document.getElementById('sk-synonyms').setAttribute('aria-invalid','true');document.getElementById('sk-synonyms').focus();return null;}const candidate={titleWeight:Number(document.getElementById('sk-title-weight').value),summaryWeight:Number(document.getElementById('sk-summary-weight').value),match:document.getElementById('sk-match').value,prefix:document.getElementById('sk-prefix').checked,recent:document.getElementById('sk-recent').checked,preferredTopic:document.getElementById('sk-preferred-topic').value,synonyms:parsed.groups};if(!skValidSettings(candidate)){error.textContent='Choose valid whole-number weights and recognized settings.';return null;}error.textContent='';document.getElementById('sk-synonyms').removeAttribute('aria-invalid');return candidate;}
function skRenderTuning(){const hits=skSearch(skIndex,document.getElementById('sk-preview-query').value,{types:[],topic:'Any topic'},skSettings);document.getElementById('sk-tuning-results').innerHTML=hits.slice(0,3).map(h=>`<a href="${skRecordUrl(h.record.id)}"><b>${skEsc(h.record.title)}</b><span>Score ${h.score} · ${h.record.type}</span></a>`).join('')||'<p>No match under the saved rules.</p>';document.getElementById('sk-config-json').textContent=JSON.stringify(skSettings,null,2);}
if(document.getElementById('sk-settings-form')){skPopulateSettings();skRenderTuning();document.getElementById('sk-preview-query').addEventListener('input',skRenderTuning);document.getElementById('sk-settings-form').addEventListener('input',()=>{document.getElementById('sk-config-status').textContent='Unsaved changes. The preview still uses saved settings.';document.getElementById('sk-config-error').textContent='';document.getElementById('sk-synonyms').removeAttribute('aria-invalid');});document.getElementById('sk-settings-form').addEventListener('submit',event=>{event.preventDefault();const candidate=skReadSettingsForm();if(!candidate)return;if(!skStore.write(candidate)){document.getElementById('sk-config-status').textContent='Storage unavailable. Settings were not saved; export the valid form as JSON instead.';return;}skSettings=candidate;skRenderTuning();document.getElementById('sk-config-status').textContent='Settings saved in this browser. The playground and preview now use these rules.';});document.getElementById('sk-export-settings').addEventListener('click',()=>{const candidate=skReadSettingsForm();if(!candidate)return;skDownload({version:1,engine:'Seek local keyword demo',settings:candidate},'seek-settings.json');document.getElementById('sk-config-status').textContent='Configuration JSON prepared for download. No external index was changed.';});document.getElementById('sk-reset-settings').addEventListener('click',()=>{const defaults=structuredClone(skDefaults);if(!skStore.write(defaults)){document.getElementById('sk-config-status').textContent='Storage unavailable. Defaults could not be saved.';return;}skSettings=defaults;skPopulateSettings();skRenderTuning();document.getElementById('sk-config-error').textContent='';document.getElementById('sk-synonyms').removeAttribute('aria-invalid');document.getElementById('sk-config-status').textContent='Default settings restored locally.';});}
function skRenderData(){const query=document.getElementById('sk-data-query').value.trim().toLowerCase();const records=skRecords.filter(r=>(r.title+' '+r.topic+' '+r.type).toLowerCase().includes(query));document.getElementById('sk-data-count').textContent=records.length+' of '+skRecords.length+' original records';document.getElementById('sk-data-list').innerHTML=records.map(r=>`<article><span class="dataset-type">${r.type}</span><div><h2><a href="${skRecordUrl(r.id)}">${skEsc(r.title)} ↗</a></h2><p>${skEsc(r.summary)}</p><span>${r.topic} · ${r.updated}</span></div><details><summary>View record JSON</summary><pre>${skEsc(JSON.stringify(r,null,2))}</pre></details></article>`).join('')||'<p class="data-empty">No indexed record matches that title or topic.</p>';}
if(document.getElementById('sk-data-query')){document.getElementById('sk-data-query').addEventListener('input',skRenderData);document.getElementById('sk-export-data').addEventListener('click',()=>{skDownload({version:1,collection:'Fieldbook fictional records',records:skRecords},'seek-fieldbook-records.json');document.getElementById('sk-data-export').textContent='Original dataset JSON prepared for download.';});skRenderData();}
if(document.getElementById('sk-record-content')){const params=new URLSearchParams(location.search);const record=skRecords.find(r=>r.id===params.get('id'));if(record){document.title=record.title+' | Seek';document.getElementById('sk-record-meta').innerHTML=`<span>${record.type}</span><span>${record.topic}</span><time>${record.updated}</time><p>Original fictional reference record.</p>`;document.getElementById('sk-record-content').innerHTML=`<span class="eyebrow">THE FIELDBOOK / ${record.type.toUpperCase()}</span><h1>${skEsc(record.title)}</h1><p class="record-summary">${skEsc(record.summary)}</p>${record.body.map(p=>`<p>${skEsc(p)}</p>`).join('')}<div class="record-source">This is original demonstration content. No external document was indexed.</div>`;const back=new URLSearchParams();for(const key of ['q','types','topic'])if(params.get(key))back.set(key,params.get(key));document.getElementById('sk-back-search').href='search.html?'+back.toString();}}
const skPrimaryQuery=document.getElementById('sk-query')||document.getElementById('sk-hero-query')||document.getElementById('sk-preview-query')||document.getElementById('sk-data-query');
if(skPrimaryQuery){document.addEventListener('keydown',event=>{const typing=['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)||document.activeElement?.isContentEditable;if(event.key==='/'&&!typing&&!event.metaKey&&!event.ctrlKey&&!event.altKey){event.preventDefault();skPrimaryQuery.focus();}if(event.key==='Escape'&&document.activeElement===skPrimaryQuery){skPrimaryQuery.value='';skPrimaryQuery.dispatchEvent(new Event('input',{bubbles:true}));}});}
if(!skReduced.matches&&'IntersectionObserver'in window){const skObserver=new IntersectionObserver(entries=>entries.forEach(entry=>{if(entry.isIntersecting){entry.target.classList.add('entered');skObserver.unobserve(entry.target);}}),{threshold:.2});document.querySelectorAll('.hero-search,.search-principles,.synonym-board').forEach(el=>skObserver.observe(el));}