[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-calc-size-animate-to-auto":32,"search-suggestions":46},[4,20],{"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,{"slug":33,"title":34,"summary":35,"html":36,"css":37,"js":38,"config":39,"doc":41},"calc-size-animate-to-auto","height: auto animation — three techniques, live",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">📏 Three ways to animate an FAQ panel open\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Pick a technique, flip to the \"long answer,\" and click the panel. Watch what actually\n      happens to the content — not just whether it moves, but whether it \u003Cem>lies\u003C\u002Fem> to you.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Technique\u003C\u002Fdiv>\n      \u003Cdiv class=\"seg\" role=\"group\" aria-label=\"Animation technique\">\n        \u003Cbutton data-mode=\"maxheight\" aria-pressed=\"true\">max-height: 150px guess\u003C\u002Fbutton>\n        \u003Cbutton data-mode=\"js\" aria-pressed=\"false\">JS scrollHeight\u003C\u002Fbutton>\n        \u003Cbutton data-mode=\"native\" aria-pressed=\"false\">interpolate-size (native)\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:14px\">\n        \u003Clabel class=\"toggle\">\n          \u003Cinput type=\"checkbox\" id=\"longContent\" \u002F>\u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>long answer (taller than the 150px guess)\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" data-preset=\"bug\">Preset: the classic bug\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"fix\">Preset: the fix\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> The panel\u003C\u002Fdiv>\n      \u003Cdiv class=\"faq\" id=\"faqWrap\">\n        \u003Cbutton class=\"faq-q\" id=\"faqBtn\" aria-expanded=\"false\">\n          \u003Cspan>Do you offer refunds?\u003C\u002Fspan>\u003Cspan class=\"chev\">▾\u003C\u002Fspan>\n        \u003C\u002Fbutton>\n        \u003Cdiv class=\"faq-a mode-maxheight\" id=\"panel\">\n          \u003Cdiv class=\"faq-a-inner\" id=\"content\">\u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top:14px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\u003Cspan id=\"statusText\">Ready — click the question above to open it.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">Pick a technique on the left first, then click the panel.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> What your own browser just did\u003C\u002Fdiv>\n      \u003Cdiv class=\"banner info show\" id=\"supportNote\">\u003C\u002Fdiv>\n      \u003Cp class=\"hint\">\n        This isn't simulated — the \"native\" mode really sets \u003Ccode>height: auto\u003C\u002Fcode> with\n        \u003Ccode>interpolate-size: allow-keywords\u003C\u002Fcode>. Whether it animates or snaps depends on\n        which engine is rendering this page right now.\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:root { interpolate-size: allow-keywords; }\n\n  .faq { border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; }\n  .faq-q {\n    width: 100%; text-align: left; background: var(--panel-2); border: none; cursor: pointer;\n    padding: 13px 14px; font-weight: 600; font-size: 0.95rem; color: var(--ink);\n    display: flex; justify-content: space-between; align-items: center; gap: 10px;\n  }\n  .faq-q .chev { transition: transform 250ms ease; flex: none; }\n  .faq-q[aria-expanded=\"true\"] .chev { transform: rotate(180deg); }\n  .faq-a { overflow: hidden; background: var(--card); }\n  .faq-a-inner { padding: 14px 16px; line-height: 1.6; font-size: 0.92rem; color: var(--muted); }\n  .faq-a-inner p { margin: 0 0 10px; }\n  .faq-a-inner p:last-child { margin-bottom: 0; }\n\n  \u002F* Technique 1: the guessed max-height ceiling — deliberately too small for \"long\" content. *\u002F\n  .mode-maxheight { max-height: 0; transition: max-height 320ms ease; }\n  .mode-maxheight.open { max-height: 150px; }\n\n  \u002F* Technique 2: JS measures the real height and animates a real number. *\u002F\n  .mode-js { height: 0; overflow: hidden; transition: height 320ms ease; }\n\n  \u002F* Technique 3: interpolate-size lets height: auto itself be the animated target. *\u002F\n  .mode-native { height: 0; transition: height 320ms ease; }\n  .mode-native.open { height: auto; }\n\n  .measure { font-family: var(--mono); font-size: 0.8rem; }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n  const SHORT = \"\u003Cp>Yes — full refund within 30 days, no questions asked.\u003C\u002Fp>\";\n  const LONG =\n    \"\u003Cp>Yes — full refund within 30 days, no questions asked. Here's the fine print people actually ask about:\u003C\u002Fp>\" +\n    \"\u003Cp>Refunds are issued to the original payment method within 5–7 business days of us receiving the request. If you paid by invoice, we credit the next billing cycle instead of reversing a charge.\u003C\u002Fp>\" +\n    \"\u003Cp>Partial-term refunds are prorated to the day, not the month — cancel on day 12 of a 30-day term and you're refunded for the remaining 18.\u003C\u002Fp>\" +\n    \"\u003Cp>This applies to every plan except the enterprise annual tier, which is refundable in the first 30 days only, per the signed order form.\u003C\u002Fp>\";\n\n  let mode = \"maxheight\";\n  let open = false;\n\n  function currentContentHTML() {\n    return $(\"longContent\").checked ? LONG : SHORT;\n  }\n\n  function setStatus(text, kind = \"info\") {\n    $(\"statusText\").textContent = text;\n    $(\"dot\").className = \"dot \" + kind;\n  }\n\n  function applyModeClass() {\n    const panel = $(\"panel\");\n    panel.classList.remove(\"mode-maxheight\", \"mode-js\", \"mode-native\");\n    panel.classList.add(\"mode-\" + mode);\n    panel.style.height = \"\"; \u002F\u002F clear any JS-set inline height from the other technique\n  }\n\n  function measureContentHeight() {\n    \u002F\u002F A hidden clone at natural height, so we can measure the \"long\"\u002F\"short\" content\n    \u002F\u002F even while the visible panel is capped or collapsed.\n    const clone = $(\"content\").cloneNode(true);\n    clone.style.position = \"absolute\";\n    clone.style.visibility = \"hidden\";\n    clone.style.width = $(\"content\").getBoundingClientRect().width + \"px\";\n    clone.style.height = \"auto\";\n    document.body.appendChild(clone);\n    const h = clone.getBoundingClientRect().height;\n    document.body.removeChild(clone);\n    return Math.round(h);\n  }\n\n  function openPanel() {\n    open = true;\n    $(\"faqBtn\").setAttribute(\"aria-expanded\", \"true\");\n    const panel = $(\"panel\");\n    const naturalHeight = measureContentHeight();\n\n    if (mode === \"maxheight\") {\n      panel.classList.add(\"open\");\n      if (naturalHeight > 150) {\n        const hidden = naturalHeight - 150;\n        setStatus(`Clipped ~${hidden}px of content. No scrollbar, no error — it just vanished.`, \"bad\");\n      } else {\n        setStatus(`Fits under the 150px guess, but the box still animates the full 300ms — same duration a much taller panel would take.`, \"warn\");\n      }\n    } else if (mode === \"js\") {\n      panel.style.height = naturalHeight + \"px\";\n      setStatus(`Measured scrollHeight (${naturalHeight}px) and animated a real number. Correct — but that measurement ran on every single toggle.`, \"good\");\n    } else {\n      panel.classList.add(\"open\");\n      const supported = CSS.supports(\"interpolate-size\", \"allow-keywords\");\n      if (supported) {\n        setStatus(`Animated height: 0 → auto directly. No JS measurement, no guessed ceiling, and it never clips.`, \"good\");\n      } else {\n        setStatus(`Your browser doesn't support interpolate-size, so this snapped open instantly instead of animating — the safe fallback, not a crash.`, \"warn\");\n      }\n    }\n  }\n\n  function closePanel() {\n    open = false;\n    $(\"faqBtn\").setAttribute(\"aria-expanded\", \"false\");\n    const panel = $(\"panel\");\n    if (mode === \"maxheight\" || mode === \"native\") {\n      panel.classList.remove(\"open\");\n    } else if (mode === \"js\") {\n      panel.style.height = \"0px\";\n    }\n    setStatus(\"Closed. Pick a technique and open it again.\", \"info\");\n  }\n\n  function togglePanel() {\n    if (open) closePanel();\n    else openPanel();\n  }\n\n  function setMode(next) {\n    mode = next;\n    closePanel();\n    applyModeClass();\n    document.querySelectorAll(\".seg button\").forEach((b) => {\n      b.setAttribute(\"aria-pressed\", String(b.dataset.mode === mode));\n    });\n    setStatus(\"Technique set to \" + labelFor(mode) + \". Click the panel to try it.\", \"info\");\n  }\n\n  function labelFor(m) {\n    return m === \"maxheight\" ? \"max-height guess\" : m === \"js\" ? \"JS scrollHeight\" : \"interpolate-size (native)\";\n  }\n\n  $(\"content\").innerHTML = SHORT;\n  applyModeClass();\n\n  $(\"faqBtn\").addEventListener(\"click\", togglePanel);\n\n  document.querySelectorAll(\".seg button\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => setMode(btn.dataset.mode));\n  });\n\n  $(\"longContent\").addEventListener(\"change\", () => {\n    const wasOpen = open;\n    if (wasOpen) closePanel();\n    $(\"content\").innerHTML = currentContentHTML();\n    if (wasOpen) requestAnimationFrame(() => requestAnimationFrame(openPanel));\n    else setStatus(($(\"longContent\").checked ? \"Long\" : \"Short\") + \" answer loaded. Open the panel to see it.\", \"info\");\n  });\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      const isBug = btn.dataset.preset === \"bug\";\n      closePanel();\n      $(\"longContent\").checked = true;\n      $(\"content\").innerHTML = LONG;\n      setMode(isBug ? \"maxheight\" : \"native\");\n      requestAnimationFrame(() => requestAnimationFrame(openPanel));\n    });\n  });\n\n  (function reportSupport() {\n    const supported = CSS.supports(\"interpolate-size\", \"allow-keywords\");\n    const el = $(\"supportNote\");\n    if (supported) {\n      el.innerHTML = \"✅ This browser supports \u003Ccode>interpolate-size: allow-keywords\u003C\u002Fcode> — the native mode above genuinely animates to \u003Ccode>height: auto\u003C\u002Fcode>.\";\n    } else {\n      el.innerHTML = \"⚠️ This browser doesn't support \u003Ccode>interpolate-size\u003C\u002Fcode> yet (expected outside Chromium 129+). The native mode above will snap open instantly instead of animating — that's the safe fallback the post describes, not a bug in this demo.\";\n    }\n  })();",{"height":40,"autorun":17},520,{"slug":33,"title":42,"excerpt":43,"coverUrl":44,"vertical":45},"I Can Finally Delete My max-height: 9999px Animation Hack","CSS transitions have never been able to animate to height: auto, because auto isn't a number the engine can interpolate. calc-size() and interpolate-size finally give it real numbers — no JavaScript, no magic-number ceiling.","\u002Fmedia\u002Fcovers\u002Fcalc-size-animate-to-auto.png",{"slug":6},[47,51,55,58,62,66,70,74,78,82,86,89],{"slug":48,"name":49,"articles":50},"webdev","Webdev",55,{"slug":52,"name":53,"articles":54},"frontend","Frontend",49,{"slug":56,"name":57,"articles":54},"javascript","Javascript",{"slug":59,"name":60,"articles":61},"css","Css",20,{"slug":63,"name":64,"articles":65},"typescript","Typescript",10,{"slug":67,"name":68,"articles":69},"performance","Performance",8,{"slug":71,"name":72,"articles":73},"grammar","Grammar",6,{"slug":75,"name":76,"articles":77},"react","React",5,{"slug":79,"name":80,"articles":81},"node","Node",4,{"slug":83,"name":84,"articles":85},"ai","Ai",3,{"slug":87,"name":88,"articles":85},"html","Html",{"slug":90,"name":91,"articles":85},"tutorial","Tutorial"]