[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-css-grid-lanes-native-masonry-layout":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-grid-lanes-native-masonry-layout","CSS Grid Lanes — native masonry, DOM order intact",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 CSS Grid Lanes: watch reading order survive masonry\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Same 9 cards, same document order every time. The dashed line traces that order,\n      1 → 9, across the screen. Switch layouts and watch what each one does to it.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Controls\u003C\u002Fdiv>\n      \u003Cdiv class=\"seg\" id=\"modeSeg\" role=\"tablist\" aria-label=\"Layout mode\">\n        \u003Cbutton type=\"button\" data-mode=\"columns\" aria-pressed=\"true\">CSS columns (the hack)\u003C\u002Fbutton>\n        \u003Cbutton type=\"button\" data-mode=\"grid\" aria-pressed=\"false\">Plain grid (safe fallback)\u003C\u002Fbutton>\n        \u003Cbutton type=\"button\" data-mode=\"lanes\" aria-pressed=\"false\">Grid Lanes (native masonry)\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\" style=\"margin-top: 12px\">\n        \u003Cbutton class=\"btn ghost\" id=\"shuffle\">Shuffle card heights\u003C\u002Fbutton>\n        \u003Clabel class=\"toggle\" style=\"margin-left: 6px\">\n          \u003Cinput type=\"checkbox\" id=\"traceToggle\" checked \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>trace reading order 1 → 9\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\">\n        Every mode shows the exact same 9 cards, numbered in the order they appear in the\n        HTML — that number never moves. Only the layout strategy changes where each card\n        lands on screen.\n      \u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Result\u003C\u002Fdiv>\n      \u003Cdiv class=\"banner\" id=\"supportBanner\">Checking your browser for native \u003Ccode>grid-lanes\u003C\u002Fcode> support…\u003C\u002Fdiv>\n      \u003Cdiv class=\"stage-wrap\">\n        \u003Csvg id=\"traceSvg\">\u003C\u002Fsvg>\n        \u003Cdiv class=\"stage mode-columns\" id=\"stage\">\u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"status\" id=\"status\" style=\"margin-top: 12px\">\n        \u003Cspan class=\"dot info\" id=\"dot\">\u003C\u002Fspan>\n        \u003Cspan id=\"statusText\">Ready.\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"hint\">Caption appears here.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">3\u003C\u002Fspan> The CSS behind this mode\u003C\u002Fdiv>\n      \u003Cpre class=\"code\" id=\"codeBlock\">\u003C\u002Fpre>\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-wrap {\n    position: relative;\n  }\n  .stage {\n    position: relative;\n    min-height: 260px;\n    padding: 14px;\n    border: 1px dashed var(--line);\n    border-radius: var(--radius-sm);\n    background: var(--panel-2);\n  }\n  .stage.mode-columns {\n    columns: 3;\n    column-gap: 14px;\n  }\n  .stage.mode-grid {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    grid-auto-rows: min-content;\n    gap: 14px;\n  }\n  .stage.mode-lanes.native {\n    display: grid-lanes;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 14px;\n  }\n  .stage.mode-lanes.sim {\n    display: block;\n  }\n  .card {\n    box-sizing: border-box;\n    border-radius: var(--radius-sm);\n    border: 1px solid var(--line-soft);\n    background: var(--card);\n    padding: 10px 12px;\n    margin-bottom: 14px;\n    break-inside: avoid;\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n  }\n  .stage.mode-grid .card,\n  .stage.mode-lanes.native .card {\n    margin-bottom: 0;\n  }\n  .stage.mode-lanes.sim .card {\n    position: absolute;\n    top: 0;\n    left: 0;\n    margin-bottom: 0;\n    transition: transform 0.35s cubic-bezier(.2,.8,.2,1);\n    width: var(--card-w, 30%);\n  }\n  .card .badge {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    border-radius: 999px;\n    background: var(--accent);\n    color: var(--accent-ink);\n    font-family: var(--mono);\n    font-weight: 800;\n    font-size: 0.82rem;\n    flex: none;\n  }\n  .card .lines {\n    margin-top: 8px;\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n  }\n  .card .lines span {\n    display: block;\n    height: 7px;\n    border-radius: 4px;\n    background: var(--line-soft);\n  }\n  #traceSvg {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    pointer-events: none;\n    z-index: 2;\n  }\n  #traceSvg path {\n    fill: none;\n    stroke: var(--accent);\n    stroke-width: 2;\n    stroke-dasharray: 5 5;\n    opacity: 0.85;\n  }\n  #traceSvg circle {\n    fill: var(--accent);\n    opacity: 0.9;\n  }","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n  const stage = $(\"stage\");\n  const svg = $(\"traceSvg\");\n  const status = $(\"statusText\");\n  const dot = $(\"dot\");\n  const hint = $(\"hint\");\n  const codeBlock = $(\"codeBlock\");\n  const supportBanner = $(\"supportBanner\");\n  const traceToggle = $(\"traceToggle\");\n\n  const N = 9;\n  const COLS = 3;\n  let heights = [60, 110, 150, 70, 130, 90, 160, 60, 100];\n  let mode = \"columns\";\n\n  const nativeLanesSupported =\n    typeof CSS !== \"undefined\" && CSS.supports && CSS.supports(\"display\", \"grid-lanes\");\n\n  function setStatus(text, kind) {\n    status.textContent = text;\n    dot.className = \"dot \" + kind;\n  }\n\n  function buildCards() {\n    stage.innerHTML = \"\";\n    for (let i = 0; i \u003C N; i++) {\n      const card = document.createElement(\"div\");\n      card.className = \"card\";\n      card.dataset.index = String(i);\n      const lineCount = Math.max(1, Math.round(heights[i] \u002F 28));\n      let linesHtml = \"\";\n      for (let l = 0; l \u003C lineCount; l++) {\n        const w = l === lineCount - 1 ? \"60%\" : \"95%\";\n        linesHtml += `\u003Cspan style=\"width:${w}\">\u003C\u002Fspan>`;\n      }\n      card.innerHTML =\n        `\u003Cspan class=\"badge\">${i + 1}\u003C\u002Fspan>\u003Cdiv class=\"lines\">${linesHtml}\u003C\u002Fdiv>`;\n      card.style.height = heights[i] + \"px\";\n      stage.appendChild(card);\n    }\n  }\n\n  \u002F\u002F Lays cards out exactly the way `display: grid-lanes` is specified to:\n  \u002F\u002F walk the items in DOM order and drop each one into whichever lane\n  \u002F\u002F currently has the least content. Used verbatim when the browser doesn't\n  \u002F\u002F support the real thing yet.\n  function simulateLanes() {\n    const cards = Array.from(stage.children);\n    const stageWidth = stage.clientWidth - 28; \u002F\u002F minus padding\n    const gap = 14;\n    const colWidth = (stageWidth - gap * (COLS - 1)) \u002F COLS;\n    const laneHeights = new Array(COLS).fill(0);\n    let maxHeight = 0;\n    cards.forEach((card) => {\n      let lane = 0;\n      for (let c = 1; c \u003C COLS; c++) {\n        if (laneHeights[c] \u003C laneHeights[lane]) lane = c;\n      }\n      const x = lane * (colWidth + gap);\n      const y = laneHeights[lane];\n      card.style.setProperty(\"--card-w\", colWidth + \"px\");\n      card.style.width = colWidth + \"px\";\n      card.style.transform = `translate(${x}px, ${y}px)`;\n      laneHeights[lane] += card.offsetHeight + gap;\n      maxHeight = Math.max(maxHeight, laneHeights[lane]);\n    });\n    stage.style.height = Math.max(0, maxHeight - gap) + 14 + \"px\";\n  }\n\n  function clearInlineLayout() {\n    Array.from(stage.children).forEach((card) => {\n      card.style.transform = \"\";\n      card.style.width = \"\";\n      card.style.height = heights[Number(card.dataset.index)] + \"px\";\n    });\n    stage.style.height = \"\";\n  }\n\n  function drawTrace() {\n    svg.innerHTML = \"\";\n    if (!traceToggle.checked) return;\n    const stageRect = stage.getBoundingClientRect();\n    const wrapRect = stage.parentElement.getBoundingClientRect();\n    const points = Array.from(stage.children).map((card) => {\n      const r = card.getBoundingClientRect();\n      return {\n        x: r.left - wrapRect.left + r.width \u002F 2,\n        y: r.top - wrapRect.top + r.height \u002F 2,\n      };\n    });\n    svg.setAttribute(\"viewBox\", `0 0 ${wrapRect.width} ${stageRect.height}`);\n    let d = \"\";\n    points.forEach((p, i) => {\n      d += (i === 0 ? \"M\" : \"L\") + p.x + \" \" + p.y + \" \";\n    });\n    const path = document.createElementNS(\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\", \"path\");\n    path.setAttribute(\"d\", d.trim());\n    svg.appendChild(path);\n    points.forEach((p) => {\n      const c = document.createElementNS(\"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg\", \"circle\");\n      c.setAttribute(\"cx\", p.x);\n      c.setAttribute(\"cy\", p.y);\n      c.setAttribute(\"r\", 4);\n      svg.appendChild(c);\n    });\n  }\n\n  const CODE = {\n    columns: `.gallery {\n  columns: 3;\n  column-gap: 14px;\n}\n.gallery > * {\n  break-inside: avoid;\n}`,\n    grid: `.gallery {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 14px;\n}`,\n    lanes: `.gallery {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 14px;\n}\n\n@supports (display: grid-lanes) {\n  .gallery {\n    display: grid-lanes;\n  }\n}`,\n  };\n\n  function render() {\n    stage.className = \"stage mode-\" + mode;\n    codeBlock.textContent = CODE[mode];\n\n    if (mode === \"columns\") {\n      clearInlineLayout();\n      setStatus(\n        \"Visual order fills column 1 top-to-bottom (1,2,3…) before column 2 even starts.\",\n        \"warn\"\n      );\n      hint.textContent =\n        \"Watch the trace line: it plunges straight down the left column, then jumps all the way back to the top for column 2. That jump is exactly what a keyboard Tab or a screen reader does too.\";\n    } else if (mode === \"grid\") {\n      clearInlineLayout();\n      setStatus(\"Reading order preserved — but every row is as tall as its tallest card.\", \"info\");\n      hint.textContent =\n        \"Grid keeps 1→9 reading left-to-right like you'd expect, but it isn't masonry: short cards leave dead space because every card in a row shares that row's height.\";\n    } else {\n      stage.classList.add(nativeLanesSupported ? \"native\" : \"sim\");\n      if (nativeLanesSupported) {\n        clearInlineLayout();\n        setStatus(\"Native display: grid-lanes is packing this — no JS involved.\", \"good\");\n        hint.textContent =\n          \"Your browser supports Grid Lanes, so this is the real algorithm: each card drops into whichever lane is currently shortest, in document order.\";\n      } else {\n        requestAnimationFrame(simulateLanes);\n        setStatus(\"Simulated with JS — your browser doesn't ship grid-lanes yet.\", \"warn\");\n        hint.textContent =\n          \"This reproduces the spec's packing rule exactly (shortest lane wins, DOM order preserved) so you can see the result even before Chrome and Firefox unflag the real thing.\";\n      }\n    }\n    requestAnimationFrame(drawTrace);\n  }\n\n  function setBanner() {\n    supportBanner.classList.add(\"show\");\n    if (nativeLanesSupported) {\n      supportBanner.classList.add(\"info\");\n      supportBanner.innerHTML =\n        \"✅ Your browser supports native \u003Ccode>display: grid-lanes\u003C\u002Fcode> — the \\\"Grid Lanes\\\" mode below is the real browser layout, not a simulation.\";\n    } else {\n      supportBanner.classList.remove(\"info\");\n      supportBanner.innerHTML =\n        \"⚠️ Your browser doesn't support \u003Ccode>display: grid-lanes\u003C\u002Fcode> yet — as of this writing only Safari ships it unflagged. The \\\"Grid Lanes\\\" mode below is a JS simulation of the exact same packing rule.\";\n    }\n  }\n\n  $(\"modeSeg\").addEventListener(\"click\", (e) => {\n    const btn = e.target.closest(\"button[data-mode]\");\n    if (!btn) return;\n    mode = btn.dataset.mode;\n    Array.from($(\"modeSeg\").children).forEach((b) =>\n      b.setAttribute(\"aria-pressed\", String(b === btn))\n    );\n    render();\n  });\n\n  $(\"shuffle\").addEventListener(\"click\", () => {\n    heights = heights.map(() => 50 + Math.round(Math.random() * 130));\n    buildCards();\n    render();\n  });\n\n  traceToggle.addEventListener(\"change\", () => requestAnimationFrame(drawTrace));\n  window.addEventListener(\"resize\", () => requestAnimationFrame(drawTrace));\n\n  setBanner();\n  buildCards();\n  render();",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"Stop Using a JS Masonry Library. CSS Grid Lanes Does It Natively.","Masonry layouts have meant a JS library or a CSS column-count hack that scrambles reading order ever since Pinterest made the look popular. CSS Grid Lanes finally does it natively — here's the real support story and how to ship it today.","\u002Fmedia\u002Fcovers\u002Fcss-grid-lanes-native-masonry-layout.png",{"slug":6},[59,63,67,71,75,79,83,87,91,95,98,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",159,{"slug":64,"name":65,"articles":66},"javascript","Javascript",129,{"slug":68,"name":69,"articles":70},"frontend","Frontend",90,{"slug":72,"name":73,"articles":74},"tutorial","Tutorial",63,{"slug":76,"name":77,"articles":78},"css","Css",48,{"slug":80,"name":81,"articles":82},"performance","Performance",27,{"slug":84,"name":85,"articles":86},"typescript","Typescript",23,{"slug":88,"name":89,"articles":90},"react","React",19,{"slug":92,"name":93,"articles":94},"programming","Programming",15,{"slug":96,"name":97,"articles":94},"browser","Browser",{"slug":99,"name":100,"articles":101},"node","Node",14,{"slug":103,"name":104,"articles":105},"html","Html",11]