[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-css-custom-highlight-api-no-dom-mutation":44,"search-suggestions":58},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"css-custom-highlight-api-no-dom-mutation","CSS Custom Highlight API — interactive playground",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 The highlight that deletes your selection\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Highlight a word, select some text yourself, then simulate a live update — and watch what\n      happens to your selection under each method. That difference is the whole post.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Step 1 — pick what to highlight\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\">\n        \u003Cinput id=\"query\" type=\"text\" placeholder=\"Search for a word…\" class=\"code\" style=\"flex:1; min-width:160px; padding:8px 10px;\" \u002F>\n        \u003Cbutton class=\"btn ghost\" id=\"reset\">Reset\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:10px\">\n        \u003Cdiv class=\"seg\" id=\"mode\">\n          \u003Cbutton data-mode=\"new\" aria-pressed=\"true\">CSS Custom Highlight API\u003C\u002Fbutton>\n          \u003Cbutton data-mode=\"old\" aria-pressed=\"false\">innerHTML + &lt;mark&gt; (the old way)\u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" data-preset=\"highlight\">Search: \"highlight\"\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"text\">Search: \"text\"\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"selection\">Search: \"selection\"\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"unsupported\" hidden>\n        Your browser doesn't support the CSS Custom Highlight API yet (needs Chrome\u002FEdge 105+, Safari 17.2+, or Firefox 140+).\n        Switch to the old-way mode to see the bug it fixes.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Step 2 — select some text, then simulate a live update\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">Drag-select a few words below with your mouse. Don't click anything else — instead, click the button underneath the paragraph. It stands in for \"an unrelated feature on the page just re-rendered.\"\u003C\u002Fp>\n      \u003Cdiv class=\"stage\" id=\"stage\">\u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:12px\">\n        \u003Cbutton class=\"btn primary\" id=\"simulate\">🔁 Simulate a live update\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top:12px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Select some text above, then click the button.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"selbox\" style=\"margin-top:8px\">\n        \u003Cspan class=\"val\">selection before: \u003Cspan id=\"before\">—\u003C\u002Fspan>\u003C\u002Fspan>\n        \u003Cspan class=\"val\">selection after: \u003Cspan id=\"after\">—\u003C\u002Fspan>\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"caption\">\n        In \u003Cstrong>CSS Custom Highlight API\u003C\u002Fstrong> mode, the re-render paints via \u003Ccode>CSS.highlights\u003C\u002Fcode> —\n        no DOM node ever moves, so your selection survives it.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n.stage {\n    min-height: 120px;\n    padding: 16px 18px;\n    border: 1px dashed var(--line);\n    border-radius: var(--radius-sm);\n    background: var(--panel-2);\n    line-height: 1.6;\n    font-size: 1.02rem;\n    user-select: text;\n  }\n  mark.old-hl {\n    background: gold;\n    color: #111;\n    border-radius: 2px;\n  }\n  ::highlight(search-demo) {\n    background-color: gold;\n    color: #111;\n  }\n  .selbox {\n    display: flex;\n    gap: 10px;\n    align-items: center;\n    flex-wrap: wrap;\n  }\n  .selbox .val {\n    font-family: var(--mono, monospace);\n    font-size: 0.85rem;\n    padding: 2px 8px;\n    border-radius: 999px;\n    background: var(--panel-2);\n    border: 1px solid var(--line);\n  }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n\n  const RAW =\n    \"Search-in-page features usually highlight every match of your query inside a block of text. \" +\n    \"A naive implementation rebuilds that text from scratch on every keystroke, wrapping each match \" +\n    \"in a highlight tag. That rebuild is invisible right up until you try to select a word yourself — \" +\n    \"then your selection disappears, because the rebuild replaced the very node it lived on.\";\n\n  const stage = $(\"stage\");\n  const supported = typeof window.Highlight === \"function\" && \"highlights\" in CSS;\n  if (!supported) {\n    $(\"unsupported\").hidden = false;\n    document.querySelector('[data-mode=\"new\"]').disabled = true;\n    document.querySelector('[data-mode=\"new\"]').setAttribute(\"aria-pressed\", \"false\");\n    document.querySelector('[data-mode=\"old\"]').setAttribute(\"aria-pressed\", \"true\");\n  }\n\n  let mode = supported ? \"new\" : \"old\";\n  stage.textContent = RAW;\n\n  function setStatus(text, kind) {\n    $(\"statusText\").textContent = text;\n    $(\"dot\").className = \"dot \" + kind;\n  }\n\n  function renderOld(query) {\n    if (!query) {\n      stage.textContent = RAW;\n      return;\n    }\n    const escaped = query.replace(\u002F[.*+?^${}()|[\\]\\\\]\u002Fg, \"\\\\$&\");\n    const re = new RegExp(\"(\" + escaped + \")\", \"gi\");\n    stage.innerHTML = RAW.replace(re, \"\u003Cmark class=\\\"old-hl\\\">$1\u003C\u002Fmark>\");\n  }\n\n  function renderNew(query) {\n    CSS.highlights.delete(\"search-demo\");\n    if (!query) return;\n    const walker = document.createTreeWalker(stage, NodeFilter.SHOW_TEXT);\n    const q = query.toLowerCase();\n    const ranges = [];\n    let node;\n    while ((node = walker.nextNode())) {\n      const text = node.textContent.toLowerCase();\n      let i = 0;\n      while ((i = text.indexOf(q, i)) !== -1) {\n        const r = new Range();\n        r.setStart(node, i);\n        r.setEnd(node, i + q.length);\n        ranges.push(r);\n        i += q.length;\n      }\n    }\n    if (ranges.length) CSS.highlights.set(\"search-demo\", new Highlight(...ranges));\n  }\n\n  \u002F\u002F Apply the current query's highlighting to the stage. Used to set up matches\n  \u002F\u002F (step 1) — never call this from a handler that must preserve the reader's\n  \u002F\u002F own text selection, since renderOld's innerHTML write is exactly the thing\n  \u002F\u002F under test.\n  function applyHighlight(query) {\n    if (mode === \"old\") {\n      CSS.highlights.delete(\"search-demo\");\n      renderOld(query);\n    } else {\n      if (stage.innerHTML.includes(\"\u003Cmark\")) stage.textContent = RAW;\n      renderNew(query);\n    }\n  }\n\n  $(\"query\").addEventListener(\"input\", (e) => applyHighlight(e.target.value));\n\n  $(\"reset\").addEventListener(\"click\", () => {\n    $(\"query\").value = \"\";\n    CSS.highlights.delete(\"search-demo\");\n    stage.textContent = RAW;\n    $(\"before\").textContent = \"—\";\n    $(\"after\").textContent = \"—\";\n    setStatus(\"Select some text above, then click the button.\", \"info\");\n  });\n\n  document.querySelectorAll(\"#mode button\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      if (btn.disabled) return;\n      document.querySelectorAll(\"#mode button\").forEach((b) => b.setAttribute(\"aria-pressed\", \"false\"));\n      btn.setAttribute(\"aria-pressed\", \"true\");\n      mode = btn.dataset.mode;\n      CSS.highlights.delete(\"search-demo\");\n      stage.textContent = RAW;\n      $(\"caption\").innerHTML =\n        mode === \"new\"\n          ? 'In \u003Cstrong>CSS Custom Highlight API\u003C\u002Fstrong> mode, the re-render paints via \u003Ccode>CSS.highlights\u003C\u002Fcode> — no DOM node ever moves, so your selection survives it.'\n          : 'In \u003Cstrong>innerHTML + &lt;mark&gt;\u003C\u002Fstrong> mode, the re-render replaces every one of the paragraph\\'s child nodes — so any selection inside it collapses, whether or not it overlapped a match.';\n      applyHighlight($(\"query\").value);\n    });\n  });\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      $(\"query\").value = btn.dataset.preset;\n      applyHighlight(btn.dataset.preset);\n    });\n  });\n\n  \u002F\u002F The selection test itself. mousedown.preventDefault() stops the browser's\n  \u002F\u002F default \"collapse selection to the click point\" behavior, so whatever the\n  \u002F\u002F reader selected in the stage survives long enough for the click handler to\n  \u002F\u002F measure it — a standard trick from rich-text-editor toolbars.\n  const simulate = $(\"simulate\");\n  simulate.addEventListener(\"mousedown\", (e) => e.preventDefault());\n  simulate.addEventListener(\"click\", () => {\n    const before = String(window.getSelection());\n    $(\"before\").textContent = before ? JSON.stringify(before) : \"(nothing selected)\";\n\n    applyHighlight($(\"query\").value);\n\n    const after = String(window.getSelection());\n    $(\"after\").textContent = after ? JSON.stringify(after) : \"(nothing selected)\";\n\n    if (before && !after) {\n      setStatus(\"Your selection was destroyed by the simulated update.\", \"bad\");\n    } else if (before && after === before) {\n      setStatus(\"Your selection survived, untouched.\", \"good\");\n    } else {\n      setStatus(\"Nothing was selected — drag-select some text above first.\", \"info\");\n    }\n  });",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"The Search Highlight That Deletes Your Selection","A search-highlight feature that rebuilds its DOM on every keystroke can wipe out a user's own text selection with no warning. The CSS Custom Highlight API fixes it by styling text ranges without ever touching a DOM node.","\u002Fmedia\u002Fcovers\u002Fcss-custom-highlight-api-no-dom-mutation.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,98,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",93,{"slug":64,"name":65,"articles":66},"javascript","Javascript",78,{"slug":68,"name":69,"articles":70},"frontend","Frontend",70,{"slug":72,"name":73,"articles":74},"css","Css",32,{"slug":76,"name":77,"articles":78},"tutorial","Tutorial",24,{"slug":80,"name":81,"articles":82},"typescript","Typescript",14,{"slug":84,"name":85,"articles":86},"performance","Performance",12,{"slug":88,"name":89,"articles":90},"react","React",11,{"slug":92,"name":93,"articles":94},"browser","Browser",8,{"slug":96,"name":97,"articles":94},"node","Node",{"slug":99,"name":100,"articles":101},"grammar","Grammar",6,{"slug":103,"name":104,"articles":101},"programming","Programming"]