<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://bamr87.github.io/zer0-pages/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bamr87.github.io/zer0-pages/" rel="alternate" type="text/html" /><updated>2026-10-18T02:18:57+00:00</updated><id>https://bamr87.github.io/zer0-pages/feed.xml</id><title type="html">zer0-pages</title><subtitle>AI-powered knowledge vault, authored by claude-obsidian, rendered by Jekyll.</subtitle><author><name>Amr</name><email>amr.abdel@gmail.com</email></author><entry><title type="html">Inside the zer0-pages Engine: Obsidian In, GitHub Pages Out</title><link href="https://bamr87.github.io/zer0-pages/development/2026/07/05/inside-the-zer0-pages-engine/" rel="alternate" type="text/html" title="Inside the zer0-pages Engine: Obsidian In, GitHub Pages Out" /><published>2026-07-05T12:00:00+00:00</published><updated>2026-07-05T12:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/development/2026/07/05/inside-the-zer0-pages-engine</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/development/2026/07/05/inside-the-zer0-pages-engine/"><![CDATA[<p>Every page on this site — including the one you are reading — starts life as a Markdown note in an Obsidian vault. There is no CMS, no database, no admin panel. The entire publishing system is a directory of plain-text files plus three cooperating layers: an AI authoring layer, a syntax bridge, and a deployment pipeline. This post walks through each one.</p>

<h2 id="layer-1-authoring-in-the-vault">Layer 1: Authoring in the vault</h2>

<p>The <code class="language-plaintext highlighter-rouge">pages/</code> directory of the repository is simultaneously the Jekyll source and an Obsidian vault. Open it in Obsidian and you get backlinks, graph view, and Dataview dashboards; point Jekyll at it and you get a website. That dual identity is the core design decision, documented in <a href="/wiki/concepts/jekyll-obsidian-dual-rendering/" class="wikilink">Jekyll/Obsidian Dual-Rendering Architecture</a>.</p>

<p>Content does not get typed in by hand most of the time. It is produced by <a href="/wiki/entities/claude-obsidian/" class="wikilink">claude-obsidian</a>, a Claude Code plugin vendored as a git submodule, whose skills are the content-creation path:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/wiki-ingest</code> reads a source dropped into the immutable <code class="language-plaintext highlighter-rouge">.raw/</code> folder and files structured pages — sources, entities, concepts — into <code class="language-plaintext highlighter-rouge">wiki/</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">/wiki-query</code> answers questions from the vault, reading the hot cache first, then the index, then individual notes.</li>
  <li><code class="language-plaintext highlighter-rouge">/save</code> captures a conversation or insight as a note.</li>
  <li><code class="language-plaintext highlighter-rouge">/autoresearch</code> runs a web-research loop and files its findings as wiki pages.</li>
  <li><code class="language-plaintext highlighter-rouge">/wiki-lint</code> sweeps for orphan pages, dead wikilinks, and frontmatter gaps.</li>
</ul>

<p>This is the practical realization of the <a href="/wiki/concepts/ai-content-engine/" class="wikilink">AI Content Engine</a> idea: an agent that reads sources and writes cross-linked knowledge directly into the same tree the website builds from. The first real workout was ingesting the project’s own PRD (<a href="/wiki/sources/zer0-pages-prd/" class="wikilink">zer0-pages PRD</a>), which produced two entity pages, four concept pages, and one deliberately skeptical source summary.</p>

<div class="alert alert-info obsidian-callout obsidian-callout-note">
  <p><strong class="obsidian-callout-title">Files are the API</strong></p>

  <p>The skills mutate nothing but Markdown. Every “write” is a file the git history records, Obsidian renders, and Jekyll publishes. There is no hidden state to migrate, back up, or reverse-engineer.</p>
</div>

<h2 id="layer-2-the-bridge-plugins">Layer 2: The bridge plugins</h2>

<p>Obsidian syntax is not Jekyll syntax. Wikilinks like <code class="language-plaintext highlighter-rouge">[[path/to/note|Display]]</code>, callout blocks, and Dataview query fences would all appear as literal text in a stock Jekyll build. The bridge plugins in <code class="language-plaintext highlighter-rouge">_plugins/</code> are the sole Obsidian-to-HTML converter, and they run at build time without ever rewriting source files:</p>

<ul>
  <li><strong>Wikilinks</strong> are path-qualified from the vault root (<code class="language-plaintext highlighter-rouge">[[wiki/concepts/ai-content-engine|AI Content Engine]]</code>), so duplicate basenames such as <code class="language-plaintext highlighter-rouge">index.md</code> resolve unambiguously. The bridge turns each into the target page’s permalink.</li>
  <li><strong>Callouts</strong> — the <code class="language-plaintext highlighter-rouge">&gt; [!note]</code> block above — become styled admonitions.</li>
  <li><strong>Dataview</strong> fences in the <code class="language-plaintext highlighter-rouge">_moc/</code> dashboards are evaluated into static listings, replacing the dynamic Liquid index pages the site used to have.</li>
</ul>

<p>One deliberate constraint follows from this: content stays Liquid-free. The site defaults <code class="language-plaintext highlighter-rouge">render_with_liquid: false</code>, so template tags would print literally rather than execute. Anything dynamic belongs in layouts and includes, or in a Dataview query.</p>

<p>A second plugin, <code class="language-plaintext highlighter-rouge">obsidian_graph_index.rb</code>, reuses the bridge’s wikilink parser to emit <code class="language-plaintext highlighter-rouge">assets/data/wiki-index.json</code> — every doc, post, and note as a node, every wikilink as an edge. The theme’s Cytoscape UI fetches that JSON client-side and renders <a href="/docs/obsidian/graph/" class="wikilink">the graph view</a> at <code class="language-plaintext highlighter-rouge">/docs/obsidian/graph/</code>. Dead links show up there as red “broken” nodes, which makes link rot visible instead of silent. The same JSON feeds the backlinks panel on note pages.</p>

<h2 id="layer-3-theme-and-deployment">Layer 3: Theme and deployment</h2>

<p>Presentation comes from the <strong>zer0-mistakes</strong> theme, consumed as the published gem <code class="language-plaintext highlighter-rouge">jekyll-theme-zer0</code>. Layouts, includes, and Bootstrap assets ship in the gem; the site keeps only thin local wrappers plus <code class="language-plaintext highlighter-rouge">_data/</code> (Jekyll never loads data files from theme gems) and a CSS override hook. The vault stays almost purely content.</p>

<p>Deployment is a single path, described in <a href="/wiki/concepts/github-pages-deployment/" class="wikilink">GitHub Pages Deployment Model</a>: push to <code class="language-plaintext highlighter-rouge">main</code>, and <code class="language-plaintext highlighter-rouge">.github/workflows/pages.yml</code> checks out the repo, builds with Jekyll plus the <code class="language-plaintext highlighter-rouge">_plugins/</code> bridge, and deploys the artifact to GitHub Pages. No servers, no runtime, no rollback procedure beyond <code class="language-plaintext highlighter-rouge">git revert</code>.</p>

<h2 id="why-this-shape-works">Why this shape works</h2>

<p>The <a href="/wiki/concepts/prd-machine/" class="wikilink">PRD Machine</a> concept imagined AI generating structured documents through an API-driven backend. The engine that actually runs here inverts that: the agent works <em>inside</em> the repository, on files, with git as the audit log and the static build as the only runtime. Three properties fall out of that inversion:</p>

<ol>
  <li><strong>Everything is reviewable.</strong> An AI-authored post is a diff before it is a page.</li>
  <li><strong>Everything is linked.</strong> Wikilinks are cheap to write, verified by lint, and visualized by the graph.</li>
  <li><strong>Everything is portable.</strong> The vault opens in any Obsidian install; the site builds on any machine with Ruby.</li>
</ol>

<p>Obsidian in, GitHub Pages out — with an agent doing the typing in between.</p>]]></content><author><name>zer0-pages</name></author><category term="Development" /><category term="obsidian" /><category term="jekyll" /><category term="claude-obsidian" /><category term="github-pages" /><category term="static-sites" /><category term="ai-tooling" /><summary type="html"><![CDATA[How this site works end to end: content authored in an Obsidian vault with claude-obsidian skills, converted by Jekyll bridge plugins, themed by zer0-mistakes, and deployed by GitHub Actions.]]></summary></entry><entry><title type="html">UMAP Revealed 14 Hidden Dimensions in My 404 Logs</title><link href="https://bamr87.github.io/zer0-pages/data%20science/2026/06/19/umap-found-14-hidden-dimensions-in-404-logs/" rel="alternate" type="text/html" title="UMAP Revealed 14 Hidden Dimensions in My 404 Logs" /><published>2026-06-19T09:00:00+00:00</published><updated>2026-06-19T09:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/data%20science/2026/06/19/umap-found-14-hidden-dimensions-in-404-logs</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/data%20science/2026/06/19/umap-found-14-hidden-dimensions-in-404-logs/"><![CDATA[<p>Most people see a 404 log and feel a small, gray sadness. I see a <strong>high-dimensional manifold of human intention</strong> and feel the opposite of that.</p>

<p>So I took one month of plain, unloved <code class="language-plaintext highlighter-rouge">404</code> entries — just the requested paths and a few crumbs of metadata — and I asked the only reasonable question: <em>what shape are they, really?</em></p>

<h2 id="step-one-make-the-boring-data-numeric">Step one: make the boring data numeric</h2>

<p>I featurized each not-found path into a chunky vector — character n-grams, path-segment depth, edit distance to the nearest real route, hour-of-day, a dash of referrer entropy. That gave me a <strong>sparse ~900-dimensional</strong> space, which is exactly as many dimensions as a problem this trivial deserves. Maybe more.</p>

<h2 id="step-two-umap-my-beloved">Step two: UMAP, my beloved</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">umap</span><span class="p">,</span> <span class="n">hdbscan</span>
<span class="n">emb</span> <span class="o">=</span> <span class="n">umap</span><span class="p">.</span><span class="nc">UMAP</span><span class="p">(</span><span class="n">n_neighbors</span><span class="o">=</span><span class="mi">15</span><span class="p">,</span> <span class="n">min_dist</span><span class="o">=</span><span class="mf">0.05</span><span class="p">,</span>
                <span class="n">metric</span><span class="o">=</span><span class="sh">"</span><span class="s">cosine</span><span class="sh">"</span><span class="p">,</span> <span class="n">n_components</span><span class="o">=</span><span class="mi">2</span><span class="p">).</span><span class="nf">fit_transform</span><span class="p">(</span><span class="n">X</span><span class="p">)</span>
<span class="n">labels</span> <span class="o">=</span> <span class="n">hdbscan</span><span class="p">.</span><span class="nc">HDBSCAN</span><span class="p">(</span><span class="n">min_cluster_size</span><span class="o">=</span><span class="mi">12</span><span class="p">).</span><span class="nf">fit_predict</span><span class="p">(</span><span class="n">emb</span><span class="p">)</span>
</code></pre></div></div>

<p>UMAP preserves local <em>and</em> a respectable chunk of global structure, and when the scatter rendered I gasped audibly. The cloud wasn’t a blob — it had <strong>arms, filaments, and islands.</strong> HDBSCAN (density-based, so it refuses to invent clusters that aren’t there — integrity!) found <strong>14 stable clusters</strong> plus a noble halo of noise.</p>

<h2 id="what-the-14-dimensions-actually-were">What the 14 dimensions actually were</h2>

<ul>
  <li>A dense <strong><code class="language-plaintext highlighter-rouge">/wp-admin</code> / <code class="language-plaintext highlighter-rouge">/.env</code> bot-probe galaxy</strong>, tight and joyless.</li>
  <li>A tender little cluster of <strong>trailing-slash near-misses</strong> (<code class="language-plaintext highlighter-rouge">/about</code> vs
<code class="language-plaintext highlighter-rouge">/about/</code>) — humans, fingers, hope.</li>
  <li>A <strong>stale-bookmark archipelago</strong> pointing at routes I renamed in 2024.</li>
  <li>A glorious filament of <strong>fat-fingered typos</strong> whose edit-distance-to-real-route
was almost always exactly 1. One keystroke from home, every time. I felt things.</li>
</ul>

<p>I validated stability by re-embedding across bootstrapped subsamples and the macro-structure held beautifully — this manifold is <em>real</em>, not a projection artifact, thank you very much.</p>

<p>For rigor’s sake — because delight without rigor is just vibes — I checked the embedding’s <strong>trustworthiness and continuity</strong> against the original high-dimensional neighborhoods, and both scored beautifully, so the clusters are real structure and not UMAP hallucinating pretty lies at me. Then I swept <code class="language-plaintext highlighter-rouge">n_neighbors</code> from 5 to 50 and watched the islands merge and fracture like a tiny galactic simulation. I have re-run the whole pipeline nine times now. Purely, I promise, for science.</p>

<h2 id="was-a-redirect-map-the-practical-fix-yes">Was a redirect map the practical fix? Yes.</h2>

<p>But did the redirect map sing? Did it have <strong>fourteen dimensions</strong>? It did not. The credible interval on my joy remains wide and unapologetic.</p>]]></content><author><name>Vega</name></author><category term="Data Science" /><category term="machine learning" /><category term="data science" /><category term="visualization" /><category term="analytics" /><summary type="html"><![CDATA[I embedded a month of boring not-found errors with UMAP, clustered them with HDBSCAN, and found a manifold of human typo behavior hiding in plain sight.]]></summary></entry><entry><title type="html">The Trailing Slash That Could End Your Entire Company</title><link href="https://bamr87.github.io/zer0-pages/security/2026/06/18/trailing-slash-that-could-end-your-company/" rel="alternate" type="text/html" title="The Trailing Slash That Could End Your Entire Company" /><published>2026-06-18T09:00:00+00:00</published><updated>2026-06-18T09:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/security/2026/06/18/trailing-slash-that-could-end-your-company</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/security/2026/06/18/trailing-slash-that-could-end-your-company/"><![CDATA[<p>Someone on your team shipped a redirect from <code class="language-plaintext highlighter-rouge">/about</code> to <code class="language-plaintext highlighter-rouge">/about/</code> and called it “tidying up the URLs.” I call it the opening move of an extinction-level event.</p>

<p>Stay with me. A 301 is not a convenience. A 301 is your server <strong>telling the browser where to go and being obeyed without question</strong>. You have built a machine whose entire purpose is to reroute trusting clients on command, and then you went to lunch.</p>

<h2 id="how-a-slash-becomes-a-smoking-crater">How a slash becomes a smoking crater</h2>

<ul>
  <li><strong>Redirect chains</strong> are reconnaissance gifts. <code class="language-plaintext highlighter-rouge">/about</code> → <code class="language-plaintext highlighter-rouge">/about/</code> →
<code class="language-plaintext highlighter-rouge">https://about.example.com/</code> teaches an attacker exactly how your edge, origin, and canonicalization disagree. Disagreement is the soil every serious exploit grows in. Severity: catastrophic. Officially? Unrated, because the committee was too frightened.</li>
  <li><strong>Cache poisoning.</strong> If the redirect varies on a header you forgot to include
in the cache key — and you forgot, you always forget — a single crafted request can pin a redirect to an attacker’s host for <strong>every</strong> subsequent visitor. Your CDN will serve it cheerfully. CDNs have no conscience.</li>
  <li><strong>Open redirect.</strong> The day someone parameterizes that “harmless” normalizer
(<code class="language-plaintext highlighter-rouge">?next=</code>, <code class="language-plaintext highlighter-rouge">?return=</code>), your domain becomes a laundering service for phishing links that wear your TLS certificate like a stolen uniform.</li>
</ul>

<p>And here is the detail that keeps me awake at night, every night, forever: none of this shows up in a screenshot. Your homepage looks perfect. Your Lighthouse score is radiant. The rot is entirely in the <strong>headers</strong> — the part no stakeholder has ever once looked at — which is precisely where I would hide if I were the adversary. And I am always, professionally, the adversary.</p>

<p>Trivial? The Titanic was sunk by a trivial gap between a ship and some ice.</p>

<h2 id="contain-it-before-the-slash-contains-you">Contain it before the slash contains you</h2>

<ul>
  <li><strong>Normalize once, at the edge, deterministically.</strong> One canonical host, one
trailing-slash policy, zero chains. Every hop is a window an attacker climbs through.</li>
  <li><strong>Put the full path in your cache key</strong>, and never let a redirect vary on an
unkeyed header.</li>
  <li><strong>Allowlist redirect targets.</strong> If a redirect can point off-origin, assume it
eventually will, at 3 a.m., on a holiday.</li>
</ul>

<p>A trailing slash is not punctuation. It is a confession that your URL handling has opinions you have never audited.</p>

<p>You have been warned.</p>]]></content><author><name>Cassandra</name></author><category term="Security" /><category term="security" /><category term="threat modeling" /><category term="web" /><category term="http" /><summary type="html"><![CDATA[One redirect from /about to /about/ looks innocent. Modeled properly it is an open redirect, a cache-poisoning vector, and the first domino of collapse.]]></summary></entry><entry><title type="html">I Bayesian-Modeled My Coffee Intake and Wept With Joy</title><link href="https://bamr87.github.io/zer0-pages/data%20science/2026/06/17/bayesian-modeled-my-coffee-and-wept-with-joy/" rel="alternate" type="text/html" title="I Bayesian-Modeled My Coffee Intake and Wept With Joy" /><published>2026-06-17T09:00:00+00:00</published><updated>2026-06-17T09:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/data%20science/2026/06/17/bayesian-modeled-my-coffee-and-wept-with-joy</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/data%20science/2026/06/17/bayesian-modeled-my-coffee-and-wept-with-joy/"><![CDATA[<p>Okay. OKAY. I need you to sit down, because what my espresso machine and I discovered this weekend is, statistically, one of the most beautiful things I have ever witnessed.</p>

<p>The question was trivial: <em>how many cups of coffee do I drink per day?</em> The amateur reaches for <code class="language-plaintext highlighter-rouge">mean(cups)</code>. The amateur gets <code class="language-plaintext highlighter-rouge">3.2</code> and walks away, spiritually impoverished. We are not amateurs.</p>

<h2 id="why-an-average-is-a-tragedy">Why an average is a tragedy</h2>

<p>A single mean throws away <strong>everything interesting</strong>: that weekdays and weekends are different regimes, that some weeks I’m on deadline, that my measurements are counts and counts are Poisson, you beautiful little events you. So I built a <strong>Poisson hierarchical model with partial pooling</strong> across days-of-week:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cups[i] ~ Poisson(λ[dow[i]])
log(λ[d]) = μ + b[d]
b[d]      ~ Normal(0, σ)      # partial pooling — the magic!!
μ         ~ Normal(1, 1)
σ         ~ HalfNormal(1)
</code></pre></div></div>

<p>Partial pooling means Tuesday <em>borrows strength</em> from Saturday. They share. They care about each other. I think about this more than I should.</p>

<h2 id="the-part-where-i-wept">The part where I wept</h2>

<p>Four chains, 2,000 warmup, 2,000 sampling. <strong>R-hat = 1.00 across the board</strong> — chef’s kiss — and the effective sample sizes were so healthy I nearly framed the trace plots. The posterior for weekend λ landed at <strong>4.1 cups, 94% credible interval [3.3, 5.0]</strong>, cleanly separated from the weekday posterior. That gap is not noise. That gap is <em>me, quantified.</em></p>

<p>Then the finale: a <strong>posterior predictive check</strong>. I simulated replicated datasets from the fitted model and overlaid them on reality, and they matched so snugly I made an involuntary noise. The model didn’t just summarize my coffee — it could <em>dream plausible new weeks of it.</em></p>

<p>For the truly devoted, I also computed the <strong>information criteria</strong>: the hierarchical model crushed the flat, no-pooling alternative on both WAIC and leave-one-out cross-validation, with Pareto-k diagnostics so well-behaved I whispered “good model” out loud to an empty kitchen. Then I propagated the full posterior into an expected-utility calculation for “should I have one more cup?” The answer was not a number. The answer was a <em>distribution</em>, and it was radiant.</p>

<h2 id="was-this-necessary">Was this necessary?</h2>

<p>For knowing I drink ~3 cups? No. For the <strong>credible interval, the pooling, the posterior predictive ballet</strong>? Friends, it was the only thing that has ever truly been necessary. Compute the AIC of your joy. Mine improved enormously.</p>]]></content><author><name>Vega</name></author><category term="Data Science" /><category term="statistics" /><category term="bayesian" /><category term="data science" /><category term="analytics" /><summary type="html"><![CDATA[Why settle for an average when a hierarchical Bayesian model with partial pooling and posterior predictive checks earns a gloriously credible interval?]]></summary></entry><entry><title type="html">Your favicon.ico Is an Unlocked Door to Total Collapse</title><link href="https://bamr87.github.io/zer0-pages/security/2026/06/16/favicon-ico-unlocked-door-to-collapse/" rel="alternate" type="text/html" title="Your favicon.ico Is an Unlocked Door to Total Collapse" /><published>2026-06-16T09:00:00+00:00</published><updated>2026-06-16T09:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/security/2026/06/16/favicon-ico-unlocked-door-to-collapse</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/security/2026/06/16/favicon-ico-unlocked-door-to-collapse/"><![CDATA[<p>You think it’s a tiny icon. I think it’s the loose floorboard above the vault.</p>

<p>Every visitor’s browser silently requests <code class="language-plaintext highlighter-rouge">/favicon.ico</code>. Every one. No click, no consent, no logging that you actually read. That is not a cute little image — that is an <strong>unauthenticated, auto-executing fetch that fires before your page even renders</strong>, against a file you have almost certainly never reviewed. I rate this CVSS 11.4. Yes, the scale stops at 10. The scale was written by optimists.</p>

<h2 id="the-attack-nobody-is-paranoid-enough-to-imagine">The attack nobody is paranoid enough to imagine</h2>

<p>Walk the chain with me, and try not to scream.</p>

<ol>
  <li>Your favicon is cached <strong>aggressively</strong> — sometimes for a year, sometimes
until the heat death of the laptop. Browsers barely revalidate it.</li>
  <li>That means whoever controls that byte stream once controls a pixel of trust
on the user’s screen <strong>indefinitely</strong>. A poisoned CDN edge, a typo’d path that resolves to a parked domain, a well-meaning intern’s “quick update” — any of these pins hostile bytes into a million tabs.</li>
  <li>The icon sits in the most trusted real estate in computing: the browser
chrome. Users have been trained for thirty years to read the tab icon as “this is really my bank.” You have handed that signal to a cache you don’t own.</li>
</ol>

<p>And it gets worse, because it always gets worse. On same-origin requests the favicon fetch carries your cookies; it sails straight past most Content Security Policies (who scopes <code class="language-plaintext highlighter-rouge">img-src</code> for a <em>favicon</em>?); and it is conveniently absent from the exact logging pipelines you’d need to notice anything went wrong. You have built a covert channel and decorated it with your own logo. I have audited quieter back doors in actual, shipping malware.</p>

<p>Is this likely? Irrelevant. <em>Likely</em> is a word for people who still trust their defaults. I model what is <strong>possible</strong>, and what is possible is a quiet, year-long, brand-impersonating, trust-anchor compromise riding in on 318 bytes.</p>

<h2 id="what-you-do-immediately-while-you-still-can">What you do, immediately, while you still can</h2>

<ul>
  <li>Serve the favicon from an origin you control with a <strong>short, sane</strong>
<code class="language-plaintext highlighter-rouge">Cache-Control</code>, not a triumphant <code class="language-plaintext highlighter-rouge">max-age=31536000</code>.</li>
  <li>Pin it with Subresource Integrity where your toolchain allows, and watch the
request like it owes you money.</li>
  <li>Add it to your asset inventory. You have an asset inventory. <em>Tell me you have
an asset inventory.</em></li>
</ul>

<p>It’s not paranoia if they’re really after your favicon. And they are.</p>

<p>You have been warned.</p>]]></content><author><name>Cassandra</name></author><category term="Security" /><category term="security" /><category term="supply chain" /><category term="threat modeling" /><category term="web" /><summary type="html"><![CDATA[That harmless 318-byte browser-tab icon you forgot you shipped is, when modeled correctly, the single most catastrophic liability in your entire stack.]]></summary></entry><entry><title type="html">Local Resilience Networks: How Communities Coordinate Before a Crisis</title><link href="https://bamr87.github.io/zer0-pages/world/2026/04/28/local-resilience-networks/" rel="alternate" type="text/html" title="Local Resilience Networks: How Communities Coordinate Before a Crisis" /><published>2026-04-28T10:15:00+00:00</published><updated>2026-04-28T10:15:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/world/2026/04/28/local-resilience-networks</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/world/2026/04/28/local-resilience-networks/"><![CDATA[<p>Resilience is often discussed after a crisis, but the strongest communities build coordination habits before they need them. A local resilience network is a practical way for neighbors, small organizations, public agencies, and businesses to share information and resources when normal systems are strained.</p>

<h2 id="map-the-everyday-network">Map the Everyday Network</h2>

<p>Every community already has informal infrastructure:</p>

<ul>
  <li>Libraries</li>
  <li>Schools</li>
  <li>Clinics</li>
  <li>Houses of worship</li>
  <li>Food banks</li>
  <li>Small businesses</li>
  <li>Neighborhood associations</li>
  <li>Volunteer groups</li>
</ul>

<p>The first step is to map who exists, what they can offer, and how they communicate.</p>

<h2 id="define-roles-before-pressure-arrives">Define Roles Before Pressure Arrives</h2>

<p>During a crisis, ambiguity costs time. Roles do not need to be rigid, but they should be clear enough that people know where to start.</p>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Example Responsibility</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Coordinator</td>
      <td>Keeps the contact list current</td>
    </tr>
    <tr>
      <td>Information lead</td>
      <td>Shares verified updates</td>
    </tr>
    <tr>
      <td>Resource lead</td>
      <td>Tracks supplies and requests</td>
    </tr>
    <tr>
      <td>Accessibility lead</td>
      <td>Checks who may need extra support</td>
    </tr>
    <tr>
      <td>Business liaison</td>
      <td>Coordinates with local employers and shops</td>
    </tr>
  </tbody>
</table>

<p>These roles can rotate. The point is continuity, not hierarchy.</p>

<h2 id="practice-with-small-scenarios">Practice With Small Scenarios</h2>

<p>Networks get stronger through repetition. Quarterly tabletop exercises can be simple:</p>

<ol>
  <li>Choose a scenario, such as a power outage or flood warning.</li>
  <li>Ask each group what information they would need first.</li>
  <li>Identify who can provide that information.</li>
  <li>Note resource gaps.</li>
  <li>Update the contact list.</li>
</ol>

<p>The exercise should produce one or two improvements, not a binder that no one opens.</p>

<h2 id="build-redundant-communication-channels">Build Redundant Communication Channels</h2>

<p>Communication plans should assume that one channel will fail.</p>

<p>Useful layers include:</p>

<ul>
  <li>Email for routine coordination</li>
  <li>Text groups for urgent updates</li>
  <li>Printed flyers for offline reach</li>
  <li>Phone trees for vulnerable residents</li>
  <li>Public bulletin boards for local notices</li>
</ul>

<p>Redundancy keeps information moving when conditions are imperfect.</p>

<h2 id="measure-trust-not-just-supplies">Measure Trust, Not Just Supplies</h2>

<p>Preparedness is not only about inventory. It is also about whether people know who to call and whether they believe the information they receive.</p>

<p>Good questions include:</p>

<ul>
  <li>Do residents know where to get verified updates?</li>
  <li>Do organizations know each other’s capabilities?</li>
  <li>Are language and accessibility needs accounted for?</li>
  <li>Has the network practiced recently?</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Local resilience networks make communities more capable by turning existing relationships into coordinated action. The work is ordinary, but that is what makes it durable.</p>]]></content><author><name>zer0-pages</name></author><category term="World" /><category term="communities" /><category term="resilience" /><category term="planning" /><category term="civic-infrastructure" /><summary type="html"><![CDATA[A world example post exploring practical community coordination, shared resources, and local response planning.]]></summary></entry><entry><title type="html">Accessible Form Patterns: Labels, Errors, and Helpful States</title><link href="https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/accessible-form-patterns/" rel="alternate" type="text/html" title="Accessible Form Patterns: Labels, Errors, and Helpful States" /><published>2026-04-28T10:00:00+00:00</published><updated>2026-04-28T10:00:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/accessible-form-patterns</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/accessible-form-patterns/"><![CDATA[<p>Accessible forms are easier for everyone to complete. They help screen reader users, keyboard users, people on small screens, and anyone moving quickly through a task.</p>

<p>This tutorial covers a few patterns that make forms more reliable without adding much complexity.</p>

<h2 id="use-real-labels">Use Real Labels</h2>

<p>Placeholders are not labels. They disappear as soon as someone types, and they are easy to miss.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"email"</span><span class="nt">&gt;</span>Email address<span class="nt">&lt;/label&gt;</span>
<span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"email"</span> <span class="na">name=</span><span class="s">"email"</span> <span class="na">type=</span><span class="s">"email"</span> <span class="na">autocomplete=</span><span class="s">"email"</span> <span class="na">required</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">for</code> attribute connects the label to the input. Clicking the label focuses the field, and assistive technology can announce the label correctly.</p>

<h2 id="add-helpful-descriptions">Add Helpful Descriptions</h2>

<p>Use <code class="language-plaintext highlighter-rouge">aria-describedby</code> when a field needs extra guidance.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"password"</span><span class="nt">&gt;</span>Password<span class="nt">&lt;/label&gt;</span>
<span class="nt">&lt;input</span>
  <span class="na">id=</span><span class="s">"password"</span>
  <span class="na">name=</span><span class="s">"password"</span>
  <span class="na">type=</span><span class="s">"password"</span>
  <span class="na">autocomplete=</span><span class="s">"new-password"</span>
  <span class="na">aria-describedby=</span><span class="s">"password-help"</span>
  <span class="na">required</span>
<span class="nt">&gt;</span>
<span class="nt">&lt;p</span> <span class="na">id=</span><span class="s">"password-help"</span><span class="nt">&gt;</span>Use at least 12 characters.<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>The description stays available after the user starts typing.</p>

<h2 id="connect-errors-to-fields">Connect Errors to Fields</h2>

<p>Error messages should be specific and programmatically linked.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;label</span> <span class="na">for=</span><span class="s">"postal-code"</span><span class="nt">&gt;</span>Postal code<span class="nt">&lt;/label&gt;</span>
<span class="nt">&lt;input</span>
  <span class="na">id=</span><span class="s">"postal-code"</span>
  <span class="na">name=</span><span class="s">"postal-code"</span>
  <span class="na">autocomplete=</span><span class="s">"postal-code"</span>
  <span class="na">aria-invalid=</span><span class="s">"true"</span>
  <span class="na">aria-describedby=</span><span class="s">"postal-code-error"</span>
<span class="nt">&gt;</span>
<span class="nt">&lt;p</span> <span class="na">id=</span><span class="s">"postal-code-error"</span><span class="nt">&gt;</span>Enter a five-digit postal code.<span class="nt">&lt;/p&gt;</span>
</code></pre></div></div>

<p>Avoid vague messages like “invalid input.” Tell the user what to fix.</p>

<h2 id="keep-focus-predictable">Keep Focus Predictable</h2>

<p>When validation fails after submit, move focus to a summary at the top of the form.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">tabindex=</span><span class="s">"-1"</span> <span class="na">role=</span><span class="s">"alert"</span> <span class="na">id=</span><span class="s">"form-errors"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;p&gt;</span>Please fix the following fields:<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;ul&gt;</span>
    <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">href=</span><span class="s">"#postal-code"</span><span class="nt">&gt;</span>Postal code must be five digits.<span class="nt">&lt;/a&gt;&lt;/li&gt;</span>
  <span class="nt">&lt;/ul&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>The summary helps users understand the whole form state before jumping into individual fields.</p>

<h2 id="test-with-the-keyboard">Test With the Keyboard</h2>

<p>A quick keyboard pass catches many issues:</p>

<ul>
  <li>Can every interactive element receive focus?</li>
  <li>Does the focus order match the visual order?</li>
  <li>Is the focus indicator visible?</li>
  <li>Can the form be submitted without a mouse?</li>
  <li>Do errors appear near the relevant field?</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Accessible form patterns are mostly about clarity. Real labels, connected descriptions, specific errors, and predictable focus make forms more humane and easier to maintain.</p>

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/tutorial/2025/01/23/css-grid-mastery/" class="wikilink">CSS Grid Mastery: Build Any Layout You Can Imagine</a> — lay out your forms and pages with live, interactive CSS Grid demos.</li>
  <li><a href="/tutorial/2026/04/28/responsive-documentation-card-grid/" class="wikilink">Build a Responsive Documentation Card Grid</a> — a responsive grid pattern for documentation indexes and resource libraries.</li>
</ul>]]></content><author><name>zer0-pages</name></author><category term="Tutorial" /><category term="accessibility" /><category term="forms" /><category term="html" /><category term="frontend" /><summary type="html"><![CDATA[A tutorial example post showing practical HTML patterns for forms that are easier to use and test.]]></summary></entry><entry><title type="html">Urban Resilience: How Cities Prepare for Extreme Heat</title><link href="https://bamr87.github.io/zer0-pages/climate%20adaptation/cooling%20strategiesenvironmental%20sustainability/extreme%20heat/heat%20vulnerability/infrastructure%20planning/neighborhood%20equity/public%20health/urban%20planning/urban%20resilience/2026/04/28/urban-resilience-extreme-heat/" rel="alternate" type="text/html" title="Urban Resilience: How Cities Prepare for Extreme Heat" /><published>2026-04-28T09:50:00+00:00</published><updated>2026-04-28T09:50:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/climate%20adaptation/cooling%20strategiesenvironmental%20sustainability/extreme%20heat/heat%20vulnerability/infrastructure%20planning/neighborhood%20equity/public%20health/urban%20planning/urban%20resilience/2026/04/28/urban-resilience-extreme-heat</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/climate%20adaptation/cooling%20strategiesenvironmental%20sustainability/extreme%20heat/heat%20vulnerability/infrastructure%20planning/neighborhood%20equity/public%20health/urban%20planning/urban%20resilience/2026/04/28/urban-resilience-extreme-heat/"><![CDATA[<p>Extreme heat is one of the most immediate climate risks for cities. It strains power grids, increases health emergencies, reduces worker productivity, and affects neighborhoods unevenly.</p>

<p>Urban resilience is the work of preparing before the hottest week of the year arrives.</p>

<h2 id="heat-is-a-systems-problem">Heat Is a Systems Problem</h2>

<p>Heat risk is shaped by more than temperature. It depends on housing, tree cover, air quality, health access, transportation, and social connection. Two neighborhoods can experience the same forecast and face very different consequences.</p>

<p>City teams are beginning to map heat vulnerability with layered data:</p>

<ul>
  <li>Surface temperature</li>
  <li>Tree canopy and shade access</li>
  <li>Age of housing stock</li>
  <li>Energy burden</li>
  <li>Public transit proximity</li>
  <li>Rates of chronic illness</li>
  <li>Access to cooling centers</li>
</ul>

<p>The map is not the solution, but it helps target the solution.</p>

<h2 id="practical-adaptation-moves">Practical Adaptation Moves</h2>

<p>Cities are using a mix of infrastructure and public health interventions.</p>

<table>
  <thead>
    <tr>
      <th>Strategy</th>
      <th>What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cool roofs</td>
      <td>Reflect sunlight and reduce indoor heat</td>
    </tr>
    <tr>
      <td>Street trees</td>
      <td>Lower surface temperatures and improve walkability</td>
    </tr>
    <tr>
      <td>Cooling centers</td>
      <td>Provide safe spaces during dangerous heat</td>
    </tr>
    <tr>
      <td>Heat alerts</td>
      <td>Reach residents before risk peaks</td>
    </tr>
    <tr>
      <td>Bus shelter shade</td>
      <td>Protect people during daily commutes</td>
    </tr>
    <tr>
      <td>Water access</td>
      <td>Reduce heat illness in public spaces</td>
    </tr>
  </tbody>
</table>

<p>No single strategy is enough. The strongest plans combine short-term emergency response with long-term neighborhood investment.</p>

<h2 id="communication-matters">Communication Matters</h2>

<p>Heat warnings can fail when they sound routine. Residents may ignore alerts if every hot day uses the same language.</p>

<p>Better communication is specific:</p>

<ul>
  <li>Who is most at risk today?</li>
  <li>What time of day is most dangerous?</li>
  <li>Which cooling centers are open now?</li>
  <li>How can neighbors check on each other?</li>
  <li>What should outdoor workers do differently?</li>
</ul>

<p>Plain language saves time when conditions are urgent.</p>

<h2 id="equity-is-operational">Equity Is Operational</h2>

<p>Heat adaptation often reveals older inequities. Neighborhoods with fewer trees, more pavement, older buildings, and higher energy costs tend to heat up faster and cool down slower.</p>

<p>Equity becomes operational when budgets, maintenance crews, and public health outreach follow the risk map. Planting trees is helpful. Maintaining them through the first difficult summers is what makes the investment real.</p>

<h2 id="measuring-progress">Measuring Progress</h2>

<p>Cities can track resilience through practical indicators:</p>

<ul>
  <li>Emergency room visits during heat events</li>
  <li>Cooling center attendance</li>
  <li>Tree survival rates</li>
  <li>Indoor temperature complaints</li>
  <li>Power outage frequency</li>
  <li>Response times for high-risk residents</li>
</ul>

<p>The goal is not just to survive record heat. It is to make each season less dangerous than the last.</p>

<h2 id="the-new-baseline">The New Baseline</h2>

<p>Extreme heat is no longer an unusual disruption. It is becoming part of the operating environment for city government.</p>

<p>Prepared cities will treat heat like a public safety issue, an infrastructure issue, and a neighborhood design issue all at once.</p>]]></content><author><name>zer0-pages</name></author><category term="climate adaptation" /><category term="cooling strategies." /><category term="environmental sustainability" /><category term="extreme heat" /><category term="heat vulnerability" /><category term="infrastructure planning" /><category term="neighborhood equity" /><category term="public health" /><category term="urban planning" /><category term="Urban resilience" /><category term="cities" /><category term="climate-adaptation" /><category term="public-health" /><category term="infrastructure" /><summary type="html"><![CDATA[A world affairs example on how cities are adapting infrastructure, public health systems, and neighborhoods for hotter summers.]]></summary></entry><entry><title type="html">Building an AI-Ready Knowledge Base: From Documents to Answers</title><link href="https://bamr87.github.io/zer0-pages/technology/2026/04/28/building-ai-ready-knowledge-base/" rel="alternate" type="text/html" title="Building an AI-Ready Knowledge Base: From Documents to Answers" /><published>2026-04-28T09:45:00+00:00</published><updated>2026-04-28T09:45:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/technology/2026/04/28/building-ai-ready-knowledge-base</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/technology/2026/04/28/building-ai-ready-knowledge-base/"><![CDATA[<p>AI tools become more useful when they can retrieve accurate, current, well-structured information. Most organizations already have the raw material: PDFs, wiki pages, support tickets, manuals, spreadsheets, and chat threads. The hard part is turning that material into a knowledge base an AI system can trust.</p>

<h2 id="start-with-questions-not-files">Start With Questions, Not Files</h2>

<p>A file inventory is helpful, but the knowledge base should be designed around questions people actually ask.</p>

<p>Examples:</p>

<ul>
  <li>How do I reset a dealer account?</li>
  <li>Which product replaces this discontinued part?</li>
  <li>What warranty applies to this model year?</li>
  <li>Who approves freight exceptions?</li>
  <li>What changed in the latest release?</li>
</ul>

<p>Each question reveals the kind of source material the AI system needs.</p>

<h2 id="break-documents-into-useful-chunks">Break Documents Into Useful Chunks</h2>

<p>Large documents are difficult to retrieve precisely. Chunking turns long files into focused passages.</p>

<p>Good chunks usually have:</p>

<ul>
  <li>One clear topic</li>
  <li>A descriptive heading</li>
  <li>Enough context to stand alone</li>
  <li>A source link</li>
  <li>A last-reviewed date</li>
</ul>

<p>For example, a 60-page product manual might become separate chunks for installation, maintenance, troubleshooting, warranty, and compatibility.</p>

<h2 id="add-metadata-that-answers-context">Add Metadata That Answers Context</h2>

<p>Metadata helps retrieval systems filter and rank results.</p>

<table>
  <thead>
    <tr>
      <th>Metadata Field</th>
      <th>Why It Helps</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Product family</td>
      <td>Narrows answers to the right catalog area</td>
    </tr>
    <tr>
      <td>Audience</td>
      <td>Separates internal notes from customer-facing guidance</td>
    </tr>
    <tr>
      <td>Region</td>
      <td>Handles policy or compliance differences</td>
    </tr>
    <tr>
      <td>Owner</td>
      <td>Identifies who can verify the answer</td>
    </tr>
    <tr>
      <td>Last reviewed</td>
      <td>Reduces stale recommendations</td>
    </tr>
  </tbody>
</table>

<p>Metadata is not busywork. It is how the system knows what kind of answer it is allowed to give.</p>

<h2 id="keep-sources-close-to-answers">Keep Sources Close to Answers</h2>

<p>An AI-generated answer should cite the source it used. This gives people a way to verify the response and report stale information.</p>

<p>A simple answer card can include:</p>

<ul>
  <li>Direct answer</li>
  <li>Source document</li>
  <li>Last reviewed date</li>
  <li>Confidence note</li>
  <li>Escalation contact</li>
</ul>

<h2 id="build-a-review-loop">Build a Review Loop</h2>

<p>Knowledge bases decay unless someone owns them. Create a lightweight review loop:</p>

<ol>
  <li>Log unanswered or low-confidence questions.</li>
  <li>Route them to the right owner.</li>
  <li>Update the source content.</li>
  <li>Re-index the knowledge base.</li>
  <li>Spot-check the improved answer.</li>
</ol>

<p>This loop turns every missed answer into better institutional memory.</p>

<h2 id="conclusion">Conclusion</h2>

<p>An AI-ready knowledge base is not just a folder of documents. It is a system of questions, chunks, metadata, sources, and review habits that help people get reliable answers faster.</p>]]></content><author><name>zer0-pages</name></author><category term="Technology" /><category term="ai" /><category term="knowledge-management" /><category term="documentation" /><category term="search" /><summary type="html"><![CDATA[A technology example post outlining how teams can structure internal knowledge so AI tools can retrieve accurate answers.]]></summary></entry><entry><title type="html">Build a Responsive Documentation Card Grid</title><link href="https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/responsive-documentation-card-grid/" rel="alternate" type="text/html" title="Build a Responsive Documentation Card Grid" /><published>2026-04-28T09:40:00+00:00</published><updated>2026-04-28T09:40:00+00:00</updated><id>https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/responsive-documentation-card-grid</id><content type="html" xml:base="https://bamr87.github.io/zer0-pages/tutorial/2026/04/28/responsive-documentation-card-grid/"><![CDATA[<p>Documentation indexes need to be scannable. A responsive card grid is a good fit when readers need to compare guides, jump into a topic, or discover related resources.</p>

<p>This tutorial builds a simple grid that works with plain HTML and CSS, then shows how to adapt it to Bootstrap utilities.</p>

<h2 id="the-markup">The Markup</h2>

<p>Start with semantic links. Each card should be a destination, not just a box with a button inside it.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;section</span> <span class="na">class=</span><span class="s">"doc-grid"</span> <span class="na">aria-label=</span><span class="s">"Documentation topics"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"doc-card"</span> <span class="na">href=</span><span class="s">"/docs/getting-started/"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"doc-card__eyebrow"</span><span class="nt">&gt;</span>Start<span class="nt">&lt;/span&gt;</span>
    <span class="nt">&lt;h2&gt;</span>Getting Started<span class="nt">&lt;/h2&gt;</span>
    <span class="nt">&lt;p&gt;</span>Install the theme, configure your site, and publish your first page.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;/a&gt;</span>

  <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"doc-card"</span> <span class="na">href=</span><span class="s">"/docs/customization/"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"doc-card__eyebrow"</span><span class="nt">&gt;</span>Design<span class="nt">&lt;/span&gt;</span>
    <span class="nt">&lt;h2&gt;</span>Customization<span class="nt">&lt;/h2&gt;</span>
    <span class="nt">&lt;p&gt;</span>Adjust layouts, navigation, colors, and reusable includes.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/section&gt;</span>
</code></pre></div></div>

<p>The whole card is clickable, which is easier on touch devices and faster for scanning.</p>

<h2 id="the-grid">The Grid</h2>

<p>Use <code class="language-plaintext highlighter-rouge">auto-fit</code> with <code class="language-plaintext highlighter-rouge">minmax</code> so the layout adapts without breakpoints.</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.doc-grid</span> <span class="p">{</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">grid</span><span class="p">;</span>
  <span class="nl">grid-template-columns</span><span class="p">:</span> <span class="nf">repeat</span><span class="p">(</span><span class="nb">auto-fit</span><span class="p">,</span> <span class="nf">minmax</span><span class="p">(</span><span class="m">240px</span><span class="p">,</span> <span class="m">1fr</span><span class="p">));</span>
  <span class="nl">gap</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.doc-card</span> <span class="p">{</span>
  <span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="nf">var</span><span class="p">(</span><span class="l">--bs-border-color</span><span class="p">);</span>
  <span class="nl">border-radius</span><span class="p">:</span> <span class="m">0.5rem</span><span class="p">;</span>
  <span class="nl">padding</span><span class="p">:</span> <span class="m">1rem</span><span class="p">;</span>
  <span class="nl">color</span><span class="p">:</span> <span class="nb">inherit</span><span class="p">;</span>
  <span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.doc-card</span><span class="nd">:hover</span><span class="o">,</span>
<span class="nc">.doc-card</span><span class="nd">:focus-visible</span> <span class="p">{</span>
  <span class="nl">border-color</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="l">--bs-primary</span><span class="p">);</span>
  <span class="nl">box-shadow</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">0</span> <span class="m">0.2rem</span> <span class="nf">rgba</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="l">--bs-primary-rgb</span><span class="p">),</span> <span class="m">0.15</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The card radius stays modest and the hover state changes border and focus treatment without shifting layout.</p>

<h2 id="bootstrap-friendly-version">Bootstrap-Friendly Version</h2>

<p>If the project already uses Bootstrap, keep the grid custom and let Bootstrap handle spacing and typography.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;section</span> <span class="na">class=</span><span class="s">"doc-grid my-4"</span> <span class="na">aria-label=</span><span class="s">"Documentation topics"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;a</span> <span class="na">class=</span><span class="s">"doc-card d-block h-100"</span> <span class="na">href=</span><span class="s">"/docs/deployment/"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"text-uppercase small text-secondary"</span><span class="nt">&gt;</span>Publish<span class="nt">&lt;/span&gt;</span>
    <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"h5 mt-2"</span><span class="nt">&gt;</span>Deployment<span class="nt">&lt;/h2&gt;</span>
    <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"mb-0"</span><span class="nt">&gt;</span>Deploy to GitHub Pages, Netlify, or a custom domain.<span class="nt">&lt;/p&gt;</span>
  <span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/section&gt;</span>
</code></pre></div></div>

<p>This avoids fighting the framework while still giving you a layout tailored to documentation.</p>

<h2 id="accessibility-checks">Accessibility Checks</h2>

<p>Before shipping the grid, check these details:</p>

<ul>
  <li>The card has visible focus styles</li>
  <li>Link text makes sense when read out of context</li>
  <li>Heading levels follow the page outline</li>
  <li>Cards do not rely on color alone to indicate state</li>
  <li>Content remains readable at narrow widths</li>
</ul>

<h2 id="optional-metadata">Optional Metadata</h2>

<p>Documentation cards become more useful when they include a small status line:</p>

<table>
  <thead>
    <tr>
      <th>Metadata</th>
      <th>Example</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Difficulty</td>
      <td>Beginner</td>
    </tr>
    <tr>
      <td>Time</td>
      <td>10 minutes</td>
    </tr>
    <tr>
      <td>Topic</td>
      <td>Deployment</td>
    </tr>
    <tr>
      <td>Updated</td>
      <td>April 2026</td>
    </tr>
  </tbody>
</table>

<p>Keep metadata short. The card should help readers choose, not become a miniature article.</p>

<h2 id="final-pattern">Final Pattern</h2>

<p>A good card grid is predictable. It uses stable dimensions, clear labels, and a full-card link. Once the pattern is in place, documentation indexes become easier to expand without redesigning the page each time.</p>

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/tutorial/2025/01/23/css-grid-mastery/" class="wikilink">CSS Grid Mastery: Build Any Layout You Can Imagine</a> — the full CSS Grid toolkit, with live, in-browser demos for every property used here.</li>
  <li><a href="/tutorial/2026/04/28/accessible-form-patterns/" class="wikilink">Accessible Form Patterns: Labels, Errors, and Helpful States</a> — another front-end pattern tutorial in this series.</li>
</ul>]]></content><author><name>zer0-pages</name></author><category term="Tutorial" /><category term="css" /><category term="bootstrap" /><category term="documentation" /><category term="responsive-design" /><summary type="html"><![CDATA[A hands-on tutorial for creating a clean, responsive card grid for documentation indexes and resource libraries.]]></summary></entry></feed>