Claude Sonnet 4.5 ============= BUILD OVERVIEW ============= This file describes how to build a SINGLE installable PWA for the OpenLang Hebrew language-learning hub. The app is installed once from its landing page and, when installed, caches every game from every category for offline use. Deliverables: four files, all at /Hebrew/: /Hebrew/index.html — the landing page AND the PWA shell /Hebrew/sw.js — service worker /Hebrew/manifest.json — web app manifest /Hebrew/icon.svg — home-screen icon The landing page serves two roles at once. In a browser, it is a normal page that lets the user click any game to play it online. On first visit, it also offers a single prominent "Install" button that installs the whole hub as a PWA. Once installed, the PWA's home-screen icon launches the same /Hebrew/index.html, which detects standalone mode and rewrites game links to point to the cached offline builds. The page is organized into one collapsible block per CATEGORY (Vocabulary, Phrases, ACA, Sign Language, etc.). Categories are defined at the bottom of this file under "CATEGORY DEFINITIONS". The service worker caches the union of all category URL lists as a single bundle — there is no per-category install. ------------- Theme & Style ------------- Use the Tailwind 'Stone' palette. Headings should be deep red and serif; body text sans-serif. Do not display any comments or notes. Links: Bold font, navy blue color. Links open in the current window. Screen width limited to 1024 pixels. Do not include any comments or notes in the index.html file. -------- Branding -------- - BRANDING: SVG logo of "文" (2.5em, deep red) followed by "OpenLang.ai" in large bold text. Links to "/". - LANG HEADER TEXT: "OpenLang.ai Hebrew" (static label, shown in the top bar to the right of BRANDING). - Page : "OpenLang.ai Hebrew". - Favicon: A data URI containing the character "文" (deep red on white). - SOURCE: Right-justified button with the text "SOURCE" that downloads the build-instructions.txt file. -------- Top Bar -------- Smartphone: BRANDING, LANG, SOURCE on a single line. Tablet: BRANDING, LANG (1.3rem), SOURCE. Desktop (strictly in this order): BRANDING, LANG (2.0rem), SOURCE. ------------------ Instruction Block ------------------ A short paragraph below the top bar, above the Install button: "Browse the categories below to play games in your browser, or install the whole hub as an app for offline use." ================= PWA & OFFLINE ================= - SELF-CONTAINED REQUIREMENT: - index.html must have zero external dependencies (no CDN scripts, no external fonts). All CSS and JS must be inline. Use system font stacks: Georgia/'Times New Roman' for serif headings, 'Helvetica Neue'/Arial for body text. - sw.js contains only plain JavaScript — no HTML, no module imports. - EXCEPTION: the Media.net ad loader script (see AD SLOT section) is allowed. - WHY REAL FILES FOR MANIFEST AND ICON: Chrome 93+ requires the manifest to be served from the same origin as the page with Content-Type: application/manifest+json. A data: URI manifest silently fails the installability check, beforeinstallprompt never fires, and the browser falls back to "Create shortcut" instead of a proper PWA install. Icon src values must also be fetchable URLs — data: URI icons fail the same check. - DO NOT ATTEMPT SINGLE-FILE WORKAROUNDS: All single-file approaches have been tried and fail. Do not attempt to reintroduce any of the following: 1. Self-registration via query string (index.html?sw): Nginx resolves Content-Type from the file extension via its types{} map before any if{} block is evaluated. .html is already mapped to text/html, so default_type inside an if{} is never reached. There is no Nginx directive that can conditionally override a known extension's Content-Type based on a query string without restructuring the location blocks in ways that break normal HTML serving. This approach is a dead end. 2. Blob URL (URL.createObjectURL): A Blob SW's script URL is blob:https://origin/<uuid>. The SW spec derives the maximum allowed scope from the script path, which for a Blob is /<uuid>/. Registering with scope /Hebrew/ fails with a DOMException. Requires a Service-Worker-Allowed response header to fix — same server-side problem, more complex. 3. data: URI as SW script URL: Browsers block SW registration from data: URIs; the origin is opaque and registration throws immediately. Since all assets are on the same HTTPS domain, sw.js is served with the correct application/javascript MIME type automatically by virtue of its .js extension. No server configuration is needed. - MANIFEST (manifest.json — static file, not generated at runtime): - "name": "OpenLang Hebrew" - "short_name": "OpenLang Hebrew" - "description": brief description of the hub - "display": "standalone" - "background_color": "#fafaf9" - "theme_color": "#991b1b" - "start_url": "/Hebrew/index.html" (absolute, hardcoded). Do not use window.location.pathname. - "icons": Two entries, both pointing to "icon.svg" (relative path). Sizes "192x192" with "purpose": "any maskable", and "512x512". - ICON (icon.svg — static file): - A plain SVG: white/stone-50 background, the "文" character centred in deep red (#991b1b), Georgia serif font. - Designed to work at both 192x192 and 512x512. - index.html MANIFEST LINK: Static <link> tag in <head> — no JavaScript needed: <link rel="manifest" href="manifest.json"> - SERVICE WORKER (sw.js): - Plain top-level JS. No IIFE, no typeof-window guard, no HTML. - All strings must use only straight ASCII apostrophes and quotes. - PAGE_URL: Derived at the top of sw.js by replacing the sw.js filename with index.html: var PAGE_URL = self.location.href.replace(/sw\.js.*$/, 'index.html'); Resolves to the exact absolute URL used as the cache key for the landing page shell. Do not use self.registration.scope. - CACHE_NAME: 'openlang-hebrew-v1'. Increment the version suffix whenever sw.js or the URL list changes — incrementing is what triggers the old cache deletion in the activate handler. - TWO URL ARRAYS — phase 1 (shell) and phase 2 (games): Derive manifest and icon absolute URLs the same way as PAGE_URL: var BASE = self.location.href.replace(/sw\.js.*$/, ''); var SHELL_URLS = [ PAGE_URL, BASE + 'manifest.json', BASE + 'icon.svg' ]; var GAME_URLS = [ <UNION of all game URLs across all categories, each transformed to use index-offline.html instead of index.html — the SW caches the offline builds> ]; Order: the GAME_URLS array should list every category's games in the order the categories appear in CATEGORY DEFINITIONS. Within a category, preserve the order of the URL rows. - broadcast helper (module-level): function broadcast(msg) { self.clients.matchAll({includeUncontrolled:true, type:'window'}) .then(function(clients) { clients.forEach(function(c) { c.postMessage(msg); }); }); } - precacheOne(cache, url): fetches and stores one URL, ignoring errors. Wraps the fetch in a 15-second timeout using Promise.race() so a hung request never blocks the download chain: function precacheOne(cache, url) { var timeout = new Promise(function(resolve) { setTimeout(resolve, 15000); }); return Promise.race([fetch(url), timeout]) .then(function(response) { if (response && response.ok) { return cache.put(url, response); } }) .catch(function() {}); } - INSTALL — cache shell only: - Open CACHE_NAME, run precacheOne for each SHELL_URLS entry. - No progress broadcast during install — the shell is small and fast; no overlay is shown at this stage. - After Promise.all, call skipWaiting(). - ACTIVATE: - Delete every cache whose name !== CACHE_NAME. - Call self.clients.claim(). - MESSAGE HANDLER — listen for {type:'CACHE_GAMES'} from the page: When the SW receives this message, cache all GAME_URLS sequentially (one at a time) with progress broadcasting. Sequential fetching prevents timeouts and SW termination on low-powered devices: var done = 0; caches.open(CACHE_NAME).then(function(cache) { return GAME_URLS.reduce(function(chain, url) { return chain.then(function() { return precacheOne(cache, url).then(function() { done++; broadcast({type:'PRECACHE_PROGRESS', done:done, total:GAME_URLS.length}); }); }); }, Promise.resolve()); }).then(function() { broadcast({type:'PRECACHE_DONE', total:GAME_URLS.length}); }); Wrap the handler in event.waitUntil() so the SW stays alive for the duration of the downloads. - FETCH HANDLER — three-path strategy: 1. Non-GET requests: pass straight to fetch(event.request); no caching. 2. Cross-origin requests: pass straight to fetch(event.request); no caching. Never store opaque responses. 3. Same-origin GET requests: cache-first. url = event.request.url (use this string as the cache key) cache.match(url) hit -> return cached response miss -> fetch from network ok: cache.put(url, response.clone()) return response failed: return cache.match(PAGE_URL) hit -> return shell (offline fallback) miss -> return new Response('Offline', {status: 503}) ===================== LANDING PAGE HTML/JS ===================== - JAVASCRIPT STRUCTURE (in index.html) — TWO separate script blocks: BLOCK A: A <script> tag placed in <head>, before </head>. Registers the beforeinstallprompt listener as early as possible so it cannot be missed. Chrome fires this event during page load, often before <body> script tags execute. Storing it on window makes it accessible to Block B. IMPORTANT: Do NOT call the prompt here — the DOM may not be ready. Only capture and store the event, then enable the install button if it already exists. var deferredPrompt = null; window.addEventListener('beforeinstallprompt', function(e) { e.preventDefault(); deferredPrompt = e; /* Enable the install button if it already exists */ var btn = document.getElementById('install-btn'); if (btn) { btn.disabled = false; btn.style.display = 'inline-flex'; } }); BLOCK B: A <script> tag at the end of <body>, after all HTML elements. Contains all remaining logic in this exact order: STEP 1 — WIRE UP ANDROID INSTALL BUTTON: By the time Block B runs, beforeinstallprompt may have already fired and stored the event in deferredPrompt. If so, enable the install button. The beforeinstallprompt listener in Block A handles the case where the event fires later. /* Enable install button if prompt already captured */ if (deferredPrompt) { var btn = document.getElementById('install-btn'); btn.disabled = false; btn.style.display = 'inline-flex'; } document.getElementById('install-btn') .addEventListener('click', function() { if (!deferredPrompt) return; var p = deferredPrompt; deferredPrompt = null; p.prompt(); p.userChoice.then(function(choice) { if (choice.outcome === 'accepted') { document.getElementById('install-btn').style.display = 'none'; startGameDownload(); } }); }); STEP 2 — DETECT iOS: /* isIOS: navigator.standalone is defined only on iOS WebKit. It is never present on Android. */ var isIOS = (typeof navigator.standalone !== 'undefined'); STEP 3 — DEFINE startGameDownload() helper: GAME_TOTAL is the total number of game URLs across all categories, emitted as a literal number by the build. function startGameDownload() { var overlay = document.getElementById('dl-overlay'); overlay.classList.add('active'); document.getElementById('dl-bar-fill').style.width = '0%'; document.getElementById('dl-sub').textContent = '0 of ' + GAME_TOTAL + ' files'; navigator.serviceWorker.ready.then(function(reg) { reg.active.postMessage({type: 'CACHE_GAMES'}); }); } STEP 4 — LISTEN FOR SW MESSAGES (progress + done): navigator.serviceWorker.addEventListener('message', function(e) { if (e.data.type === 'PRECACHE_PROGRESS') { var pct = Math.round(e.data.done / e.data.total * 100); document.getElementById('dl-bar-fill').style.width = pct + '%'; document.getElementById('dl-sub').textContent = e.data.done + ' of ' + e.data.total + ' files'; } if (e.data.type === 'PRECACHE_DONE') { document.getElementById('dl-bar-fill').style.width = '100%'; document.getElementById('dl-sub').textContent = e.data.total + ' of ' + e.data.total + ' files'; setTimeout(function() { document.getElementById('dl-overlay').classList.remove('active'); localStorage.setItem('games-cached', '1'); }, 600); } }); STEP 5 — REGISTER SERVICE WORKER: if ('serviceWorker' in navigator) { var swURL = window.location.pathname.replace(/[^/]+$/, '') + 'sw.js'; navigator.serviceWorker.register(swURL, {scope: './'}).catch(function(){}); } STEP 6 — IOS STANDALONE LAUNCH (first launch from home screen icon): If running as installed PWA on iOS and games not yet cached, start download immediately: if (isIOS && navigator.standalone === true && !localStorage.getItem('games-cached')) { startGameDownload(); } STEP 7 — IOS INSTALL BUTTON (shown in Safari, not standalone): On iOS, repurpose the install button: instead of calling deferredPrompt.prompt() (which doesn't exist on iOS), open a modal popup with Add-to-Home-Screen instructions for iOS 26. Show the button when: - isIOS is true - navigator.standalone is false (user is in Safari, not the installed app) if (isIOS && navigator.standalone !== true) { var btn = document.getElementById('install-btn'); btn.disabled = false; btn.style.display = 'inline-flex'; /* Replace the click handler set in STEP 1 with the iOS modal opener. Using cloneNode is the simplest way to drop any previously attached listeners. */ var clone = btn.cloneNode(true); btn.parentNode.replaceChild(clone, btn); clone.addEventListener('click', function() { document.getElementById('ios-modal').classList.add('visible'); }); } document.getElementById('ios-modal-close') .addEventListener('click', function() { document.getElementById('ios-modal').classList.remove('visible'); }); /* Also close modal if user taps the backdrop */ document.getElementById('ios-modal') .addEventListener('click', function(e) { if (e.target === this) { this.classList.remove('visible'); } }); STEP 8 — STANDALONE LINK REWRITING: The URL list uses index.html so browser users get the normal online page. When the page is running as an installed PWA (standalone mode), rewrite all game card links to point to index-offline.html instead, so the service worker serves the cached offline build. Also hide the install button in standalone mode — there is nothing left to install. var isStandalone = window.matchMedia('(display-mode: standalone)').matches || (navigator.standalone === true); if (isStandalone) { document.querySelectorAll('.game-card').forEach(function(a) { a.href = a.href.replace(/\/index\.html$/, '/index-offline.html'); }); var btn = document.getElementById('install-btn'); if (btn) btn.style.display = 'none'; } - INSTALL BUTTON (in index.html): A single, prominent install button placed below the top bar and instruction block, ABOVE the first category block. This is the one-click entry point for both Android and iOS. On Android it fires the native install prompt. On iOS it opens a modal with Add-to-Home-Screen instructions. HTML (place below the instruction block): <div id="install-bar"> <button id="install-btn" style="display:none" disabled> <span class="install-btn-icon">⬇</span> <span class="install-btn-text">Install OpenLang Hebrew for offline use</span> </button> </div> The button is hidden by default (display:none, disabled). It is shown by JS only when: - beforeinstallprompt has fired (Android), OR - isIOS is true and the app is not already in standalone mode. If neither condition is met (e.g. desktop Chrome with no install prompt, or the app is already installed), the button stays hidden and takes no space. CSS: #install-bar: display:flex, justify-content:center, margin: 0.75rem 0 1rem 0. #install-btn: background:#991b1b, color:#fff, border:none, padding:0.7rem 1.4rem, border-radius:8px, font-weight:700, font-size:1rem, cursor:pointer, display:inline-flex, align-items:center, gap:0.5rem, box-shadow: 0 1px 3px rgba(0,0,0,0.12). On hover: filter:brightness(1.08). On disabled: opacity:0.5, cursor:default. .install-btn-icon: font-size:1.1rem, line-height:1. .install-btn-text: font-family: 'Helvetica Neue', Arial, sans-serif. - DOWNLOAD PROGRESS OVERLAY (in index.html): The overlay is shown in the foreground immediately after the user accepts the Android install prompt or on the first launch from the home screen on iOS. It blocks the categories until all games from every category are cached. It is never shown to users who browse without installing. HTML (place inside <body>, before the category blocks): <div id="dl-overlay"> <div id="dl-title">Downloading games for offline use...</div> <div id="dl-bar-wrap"><div id="dl-bar-fill"></div></div> <div id="dl-sub">0 of GAME_TOTAL files</div> </div> CSS: #dl-overlay: display:none, position:fixed, inset:0, high z-index, near-opaque stone-50 background, flex column centred. #dl-overlay.active: display:flex. #dl-bar-wrap: width:min(380px,80vw), height:10px, stone-200 background, border-radius:9999px, overflow:hidden. #dl-bar-fill: height:100%, initial width:0%, red background, border-radius:9999px, transition: width 0.35s ease. - IOS INSTALL MODAL (in index.html): Place the modal at the end of <body>, before closing </body> tag. The install button in its iOS mode (see STEP 7) opens this modal instead of firing a native prompt. Modal HTML: <div id="ios-modal"> <div id="ios-modal-box"> <div id="ios-modal-title">Install OpenLang Hebrew</div> <div id="ios-install-msg">To install the app on an iPhone running iOS 26, open this page in Safari. Tap the three dots menu at the bottom of the screen, then Share. Scroll down the share menu and tap “Add to Home Screen”. Finally, confirm the app’s name and tap “Add” in the top right corner. </div> <button id="ios-modal-close">Close</button> </div> </div> CSS: #ios-modal: display:none, position:fixed, inset:0, z-index:2000, background:rgba(0,0,0,0.55), align-items:center, justify-content:center. #ios-modal.visible: display:flex. #ios-modal-box: background:#fff, border-radius:12px, padding:1.5rem, max-width:320px, width:90%, display:flex, flex-direction:column, gap:1rem, text-align:center. #ios-modal-title: font-family:Georgia serif, font-size:1.1rem, font-weight:700, color:#991b1b. #ios-install-msg: font-size:0.95rem, color:stone-700, line-height:1.5. #ios-modal-close: background:stone-200, border:none, padding:0.4rem 1rem, border-radius:6px, cursor:pointer, font-size:0.9rem, font-weight:600, align-self:center. --------------- Category Icons --------------- Each category has an "Icon:" metadata field in its CATEGORY DEFINITIONS block whose value is one of the icon names listed below. The page must render that icon as an inline SVG inside the collapsible block's <summary>, to the LEFT of the title. Each icon is a small square clip-art-style SVG, 44x44 pixels, drawn in a TWO-TONE style: - A circular stone-300 (#d6d3d1) filled background. - The figure drawn on top of the background in deep red (#991b1b), as filled shapes and/or thick strokes (stroke width 2.5-3 px). No outlines around the background circle. No gradients. No additional colors. - The figure should fill roughly the central 70% of the background circle, leaving a small visible margin of stone on all sides. - viewBox "0 0 44 44", width and height attributes 44. - Use stroke-linecap="round" and stroke-linejoin="round" on any stroked paths so the figures look soft, not jagged. Icon definitions (draw each icon to match the description): Icon name: vocabulary Description: A single open book seen from above, with two visible pages and a faint vertical line down the centre for the spine. Represents a single-word vocabulary list. Keep it simple — no text on the pages, no decoration. Icon name: phrases Description: A speech bubble (rounded rectangle with a small tail pointing down-left) containing three short horizontal lines stacked vertically, suggesting lines of text. Represents spoken or written phrases. Icon name: aac Description: A 2x2 grid of four small rounded squares, evenly spaced, suggesting an AAC (Augmentative and Alternative Communication) symbol board where users tap pictograms to communicate. Each cell is filled deep red; the gaps between cells show the stone background through. Adding more categories later: when a new category is added with an Icon: name that doesn't appear in the list above, add a new "Icon name:" definition to this section before adding the category. If a category has no Icon: field, omit the icon element entirely for that block (no placeholder, no empty space). ----------------- Category Blocks ----------------- - One collapsible block per category in CATEGORY DEFINITIONS, in the same order they appear at the bottom of this file. - Use the native <details> / <summary> elements — no JavaScript needed for the collapse/expand behaviour. - ALL blocks are collapsed by default. Do NOT add the `open` attribute to any <details> element. Block structure (per category): <details class="cat-block"> <summary class="cat-summary"> <div class="cat-summary-header"> <span class="cat-icon"> <!-- Inline SVG for the icon named in this category's "Icon:" metadata field, drawn per the Category Icons section above. Omit this <span> entirely if the category has no Icon: field. --> </span> <span class="cat-title">CATEGORY_NAME</span> <span class="cat-count">N games</span> </div> <dl class="cat-meta"> <!-- one <dt>/<dd> pair per metadata field in this category's CATEGORY DEFINITIONS block, in the order they appear. Skip this entire <dl> if the category has no metadata. --> <dt>LABEL</dt> <dd>VALUE</dd> ... </dl> </summary> <div class="cat-body"> <div class="game-grid"> <!-- one card per URL in that category's URL List --> <a class="game-card" href="<URL>"> <div class="game-icon">ICON_TEXT</div> <div class="game-name">NAME</div> </a> ... </div> </div> </details> Notes: - CATEGORY_NAME is the "Category:" value from the definitions block. - N in "N games" is the count of URL rows in that category's URL list. - The <dl class="cat-meta"> contains one <dt>/<dd> pair per metadata field in the category's CATEGORY DEFINITIONS block. A metadata field is any line between "Category:" and "Here is the URL List:" that matches the pattern "Label: value" (colon-separated). The label goes in <dt>, the value goes in <dd>. Emit fields in the order they appear in the definitions. If a category has no metadata fields, omit the entire <dl class="cat-meta"> element (do not emit an empty <dl>). - EXCEPTION: the "Icon:" field is structural (it selects a category icon from the Category Icons section) and must NOT appear in the rendered <dl>. Skip it when emitting metadata. If Icon: is the only metadata field on a category, that category has no rendered metadata and the <dl> must be omitted entirely. - Metadata values may contain inline HTML (for example <strong>, <em>, <br>, <code>, <a>). Pass these through to the rendered output verbatim — do NOT escape the angle brackets. Do NOT add any HTML that wasn't in the source value. - There is NO per-category install button. Installation is a single hub-level action performed from the install button at the top of the page (see INSTALL BUTTON section above). -------------- Game Grid -------------- Inside each category block's <div class="cat-body">: - 3 columns on mobile, 3 on tablet, 4 on desktop. - Each card: 60x60px rounded-xl deep-red icon square containing the Icon Text in white, with the game Name below the icon. - Hover: subtle scale-105 and shadow enhancement. - Links open in the current window. - Links must use absolute URLs as provided in the URL list; standalone link rewriting (STEP 8) swaps index.html for index-offline.html at runtime when the app is launched from the home screen. ---------------------- Category Block Styling ---------------------- .cat-block: stone-50 background, 1px stone-200 border, border-radius 8px, margin-bottom 0.75rem, overflow hidden. .cat-summary: cursor pointer, padding 0.9rem 1.1rem, display flex, flex-direction column, gap 0.5rem. (Native <summary> elements have a default marker that interacts awkwardly with flex column layout — set list-style: none on the summary and use ::-webkit-details-marker { display: none } to hide it, then draw a disclosure triangle manually in the header row using a ::before pseudo-element on .cat-summary-header, or leave the native marker and accept its position.) .cat-summary-header: display flex, align-items center, justify-content space-between, gap 0.75rem, font-family Georgia serif, font-size 1.15rem, font-weight 700, color #991b1b. .cat-icon: flex 0 0 auto, width 44px, height 44px, display inline-flex, align-items center, justify-content center. The inner <svg> should fill this box (width:100%, height:100%). .cat-title: flex-grow 1. .cat-count: font-family sans-serif, font-size 0.8rem, font-weight 400, color stone-500, text-transform uppercase, letter-spacing 0.06em. .cat-meta: margin 0, padding 0, display grid, grid-template-columns max-content 1fr, column-gap 0.75rem, row-gap 0.25rem, font-family sans-serif, font-size 0.85rem, line-height 1.4, color stone-700. .cat-meta dt: margin 0, font-weight 600, color stone-500, text-transform uppercase, letter-spacing 0.04em, font-size 0.72rem, align-self center. .cat-meta dd: margin 0, color stone-700. .cat-meta dd strong: font-weight 700, color stone-900. .cat-meta dd em: font-style italic. .cat-meta dd a: color #1e3a8a, font-weight 600, text-decoration none. .cat-body: padding 1rem 1.1rem 1.1rem 1.1rem, border-top 1px solid stone-200. -------- Ad Slot -------- Include ONE ad slot on the page, placed between the install button and the first category block. The slot spans the full content width (max 1024px). The slot contains a Media.net ad unit. Use a placeholder publisher ID of "123456789" — this will be replaced with the real ID at launch. Media.net requires two script tags: one global loader in <head> and one per-slot inline script in the ad div. In <head> (alongside other meta tags): <script src="//contextual.media.net/dmedianet.js?cid=123456789" async></script> HTML for the ad slot: <div class="ad-slot"> <div class="ad-label">Reserved for future ads</div> <div id="media-net-ad"> <script> window._mNHandle = window._mNHandle || {}; window._mNHandle.queue = window._mNHandle.queue || []; window._mNHandle.queue.push(function() { window._mNDetails.loadTag("123456789", "320x50", "media-net-ad"); }); </script> </div> </div> CSS for the ad slot: .ad-slot: width: 100%, display: flex, flex-direction: column, align-items: center, justify-content: center, gap: 0.25rem, background: stone-100, border: 1px dashed stone-300, border-radius: 8px, padding: 0.4rem 0, margin: 0.25rem 0. .ad-label: font-size: 0.65rem, color: stone-400, font-family: sans-serif, letter-spacing: 0.08em, text-transform: uppercase, pointer-events: none. #media-net-ad: min-height: 50px, width: 320px, max-width: 100%. =============================================================================== CATEGORY DEFINITIONS =============================================================================== Each category block starts with a "Category:" line and ends at the next "Category:" line or at end-of-file. Between the Category line and the "Here is the URL List:" line, any number of optional metadata fields may appear, each on its own line in the form: Label: value The label is free-form text (e.g. "Tagline", "Level", "Word count", "Author"). The value may contain inline HTML tags such as <strong>, <em>, <br>, <code>, and <a> — these pass through to the rendered page verbatim. Metadata fields are rendered inside the collapsible block's <summary> as a <dl class="cat-meta"> list, in the order they appear here. The "Icon:" field is a special structural field that selects a category icon from the Category Icons section above; it is NOT rendered in the metadata list. Category: Vocabulary Icon: vocabulary Tagline: <strong>Core vocabulary</strong> for beginners through intermediate learners Level: A1 – B1 Word count: ~600 words across 4 levels Here is the URL List, along with the Name and Icon Text: URL,Name,Icon Text https://openlang.ai/Hebrew/Vocabulary/Beginner-1/index.html,Beginner 1,A1 https://openlang.ai/Hebrew/Vocabulary/Beginner-2/index.html,Beginner 2,A2 https://openlang.ai/Hebrew/Vocabulary/Beginner-3/index.html,Beginner 3,A3 https://openlang.ai/Hebrew/Vocabulary/Beginner-4/index.html,Beginner 4,A4 Category: Phrases Icon: phrases Tagline: <strong>Idiomatic phrases, neologisms, and quotations</strong> Level: A2 – C1 Contents: Phrases of varying length, plus modern coinages and political/religious quotations Here is the URL List, along with the Name and Icon Text: URL,Name,Icon Text https://openlang.ai/Hebrew/Phrases/Idiomatic-Phrases/index.html,Idiomatic Phrases,P1 https://openlang.ai/Hebrew/Phrases/Short-Phrases/index.html,Short Phrases,P2 https://openlang.ai/Hebrew/Phrases/Longer-Phrases/index.html,Longer Phrases,P3 https://openlang.ai/Hebrew/Phrases/Longest-Phrases/index.html,Longest Phrases,P4 https://openlang.ai/Hebrew/Phrases/Neologisms/index.html,Neologisms,N1 https://openlang.ai/Hebrew/Phrases/Leftist/index.html,Leftist Quotes,Q1 https://openlang.ai/Hebrew/Phrases/Capitalist/index.html,Capitalist Quotes,Q2 https://openlang.ai/Hebrew/Phrases/Religious/index.html,Religious Quotes,Q4