[object Object]

← back to Bubbesblock

Wire BubbesBlock live: Express server + data-driven block feed & post detail, seeded with religious-theme posts

d0b4a97a80d002b5b527d5a46080c3b5865be1c6 · 2026-06-19 14:16:28 -0700 · Steve

Files touched

Diff

commit d0b4a97a80d002b5b527d5a46080c3b5865be1c6
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jun 19 14:16:28 2026 -0700

    Wire BubbesBlock live: Express server + data-driven block feed & post detail, seeded with religious-theme posts
---
 data/posts.json   | 146 ++++++++++
 package-lock.json | 827 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.json      |  13 +
 public/chrome.js  | 118 ++++++++
 public/index.html | 385 +++----------------------
 public/post.html  |  73 +++++
 public/styles.css | 150 ++++++++++
 server.js         |  46 +++
 8 files changed, 1406 insertions(+), 352 deletions(-)

diff --git a/data/posts.json b/data/posts.json
new file mode 100644
index 0000000..cf20d52
--- /dev/null
+++ b/data/posts.json
@@ -0,0 +1,146 @@
+{
+  "neighborhood": "Beekman Ave · Sunset Park",
+  "me": { "name": "Steve Abrams", "initials": "SA", "address": "112 Beekman Ave", "verified": true, "color": "a1" },
+  "posts": [
+    {
+      "id": "honey-cake",
+      "author": "Bubbe Ruth Bernstein",
+      "initials": "RB",
+      "color": "a3",
+      "address": "108 Beekman Ave",
+      "time": "3h",
+      "edited": true,
+      "category": "🍯 Cup of Sugar · Free to a neighbor",
+      "catColor": "green",
+      "body": [
+        "<b>Made too much honey cake again.</b> 🍯 I always tell myself \"Ruth, half a recipe!\" and then I make a recipe and a half. So — there are <b>three loaves</b> on my stoop at 108 Beekman, wrapped and labeled, free for whoever wants one.",
+        "Take one, leave the foil for the next bubbe. And if anyone has a folding table I can borrow for Sunday's block party, I'll trade you a loaf for it. Knock anytime, I'm home. ♥"
+      ],
+      "photo": { "emoji": "🍯🍰", "tag": "📍 108 Beekman Ave · the famous stoop", "bg": "warm" },
+      "reactions": ["❤️", "😋", "👍"],
+      "reactedBy": "Goldie, Sol & 41 neighbors",
+      "shares": 6,
+      "comments": [
+        { "author": "Goldie Fein", "initials": "GF", "color": "a2", "nb": "114 Beekman", "time": "2h", "thanks": 12, "body": "You are a treasure, Ruth. I'll send my grandson over for a loaf — and he'll bring the folding table back to you Saturday, promise. 🙏" },
+        { "author": "Bubbe Ruth Bernstein", "initials": "RB", "color": "a3", "nb": "author", "time": "2h", "thanks": 8, "reply": true, "body": "Deal! Tell him there's an extra slice in it for him. 😉" },
+        { "author": "Sol Katz", "initials": "SK", "color": "a4", "nb": "121 Beekman", "time": "1h", "thanks": 23, "body": "This block has the best stoop in Sunset Park, hands down. Saving one for Mrs. Adler at 110, she doesn't get out much these days." }
+      ],
+      "moreComments": 15
+    },
+    {
+      "id": "shabbat-dinner",
+      "author": "Steve Abrams",
+      "initials": "SA",
+      "color": "a1",
+      "address": "112 Beekman Ave",
+      "time": "5h",
+      "edited": false,
+      "category": "🕯️ Shabbat Dinner Club · Open invite",
+      "catColor": "plum",
+      "body": [
+        "<b>Shabbat dinner this Friday at 6:30, our place — all neighbors welcome.</b> 🕯️🍷 We do this every week and there's always room for two more at the table. No need to bring anything, just yourself (a bottle of wine never hurts though).",
+        "If you've never been, this is the easiest way to meet the block. We light candles at sundown, eat way too much, and the kids run wild in the backyard. RSVP here or just knock."
+      ],
+      "photo": { "emoji": "🕯️🍞🍷", "tag": "📍 112 Beekman Ave · the long table", "bg": "candle" },
+      "reactions": ["❤️", "🙏", "🕯️"],
+      "reactedBy": "Ruth, Goldie & 28 neighbors",
+      "shares": 9,
+      "comments": [
+        { "author": "Miriam Stein", "initials": "MS", "color": "a5", "nb": "116 Beekman", "time": "4h", "thanks": 14, "body": "We'll be there! Bringing my mother's challah — double batch. Shabbat Shalom 🕊️" },
+        { "author": "Sol Katz", "initials": "SK", "color": "a4", "nb": "121 Beekman", "time": "3h", "thanks": 6, "body": "Count me in. Last week's brisket is still on my mind." }
+      ],
+      "moreComments": 11
+    },
+    {
+      "id": "sukkah-help",
+      "author": "David Reuben",
+      "initials": "DR",
+      "color": "a4",
+      "address": "119 Beekman Ave",
+      "time": "8h",
+      "edited": false,
+      "category": "🌿 Block Events · Need a hand",
+      "catColor": "green",
+      "body": [
+        "Putting up the <b>sukkah</b> in the side yard this weekend and could use one extra set of hands with the bamboo roof. 🌿 Should take an hour. In exchange: lunch, and anyone on the block is welcome to use it all week — bring your own etrog. 🍋",
+        "Sunday morning, 9am. Tools provided. The kids love decorating it so send them over."
+      ],
+      "photo": { "emoji": "🌿🍋", "tag": "📍 119 Beekman Ave · the side yard", "bg": "green" },
+      "reactions": ["👍", "🌿", "❤️"],
+      "reactedBy": "Steve, Ruth & 16 neighbors",
+      "shares": 3,
+      "comments": [
+        { "author": "Steve Abrams", "initials": "SA", "color": "a1", "nb": "112 Beekman", "time": "7h", "thanks": 5, "body": "I've got you, David. I'll bring my ladder and the good staple gun." }
+      ],
+      "moreComments": 4
+    },
+    {
+      "id": "lost-cat",
+      "author": "Goldie Fein",
+      "initials": "GF",
+      "color": "a2",
+      "address": "114 Beekman Ave",
+      "time": "10h",
+      "edited": false,
+      "category": "🐱 Lost & Found · Please share",
+      "catColor": "amber",
+      "body": [
+        "<b>Found: orange tabby</b> hanging around my back steps since this morning, very friendly, no collar. 🐱 He's safe and fed (couldn't help myself). If he's yours or you know whose he is, message me. He answers to absolutely nothing so far."
+      ],
+      "photo": { "emoji": "🐱", "tag": "📍 near 116 Beekman Ave", "bg": "amber" },
+      "reactions": ["❤️", "😺", "🙏"],
+      "reactedBy": "Miriam, David & 19 neighbors",
+      "shares": 12,
+      "comments": [
+        { "author": "Miriam Stein", "initials": "MS", "color": "a5", "nb": "116 Beekman", "time": "9h", "thanks": 7, "body": "That's Marmalade! He belongs to the Adlers at 110 — I'll let them know right now. Thank you for feeding him 🧡" }
+      ],
+      "moreComments": 13
+    },
+    {
+      "id": "block-party",
+      "author": "Sol Katz",
+      "initials": "SK",
+      "color": "a4",
+      "address": "121 Beekman Ave",
+      "time": "1d",
+      "edited": true,
+      "category": "🎉 Block Events · This Sunday",
+      "catColor": "plum",
+      "body": [
+        "<b>Beekman Ave Block Party — this Sunday, 1pm to whenever.</b> 🎉 Street's closed to traffic from 1. Potluck tables out front of 121. Bring a dish, a chair, and your bubbe.",
+        "We've got the grill, Ruth's got dessert (obviously), and Mr. Reuben is bringing the good speaker. 32 households in so far — let's make it the whole block."
+      ],
+      "photo": { "emoji": "🎉🌭🎈", "tag": "📍 Beekman Ave · street closed 1pm", "bg": "warm" },
+      "reactions": ["🎉", "❤️", "👍"],
+      "reactedBy": "Steve, Goldie, Ruth & 49 neighbors",
+      "shares": 21,
+      "comments": [
+        { "author": "Steve Abrams", "initials": "SA", "color": "a1", "nb": "112 Beekman", "time": "22h", "thanks": 18, "body": "We'll bring two folding tables and a cooler of seltzer. Can't wait 🙌" }
+      ],
+      "moreComments": 17
+    },
+    {
+      "id": "who-lived-here",
+      "author": "Miriam Stein",
+      "initials": "MS",
+      "color": "a5",
+      "address": "116 Beekman Ave",
+      "time": "1d",
+      "edited": false,
+      "category": "🏚️ Who Lived Here · History",
+      "catColor": "green",
+      "body": [
+        "Doing a little digging on the block's history and found that <b>104 Beekman</b> was a tailor's shop in the 1950s — anyone remember the Lefkowitz family? 🧵 Pulled the old records over on wholivedthere.com and there's a 1961 photo of the storefront.",
+        "Would love to put together a little block history wall for the party. If your house has a story, drop it here."
+      ],
+      "photo": { "emoji": "🏚️🧵", "tag": "📍 104 Beekman Ave · c. 1961", "bg": "sepia" },
+      "reactions": ["❤️", "👏", "🕯️"],
+      "reactedBy": "Ruth, Sol & 11 neighbors",
+      "shares": 4,
+      "comments": [
+        { "author": "Bubbe Ruth Bernstein", "initials": "RB", "color": "a3", "nb": "108 Beekman", "time": "1d", "thanks": 9, "body": "Oh, the Lefkowitzes! Mr. L hemmed my wedding dress in 1968. This block has so much in it." }
+      ],
+      "moreComments": 6
+    }
+  ]
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..55d6274
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,827 @@
+{
+  "name": "bubbesblock",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "bubbesblock",
+      "version": "0.1.0",
+      "dependencies": {
+        "express": "^4.19.2"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.5",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+      "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.15.1",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+      "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.5",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.15.1",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+      "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4",
+        "side-channel-list": "^1.0.1",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6b96afd
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "bubbesblock",
+  "version": "0.1.0",
+  "description": "BubbesBlock — Nextdoor-style block community surface for the home-history ecosystem",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js",
+    "dev": "node server.js"
+  },
+  "dependencies": {
+    "express": "^4.19.2"
+  }
+}
diff --git a/public/chrome.js b/public/chrome.js
new file mode 100644
index 0000000..2287cec
--- /dev/null
+++ b/public/chrome.js
@@ -0,0 +1,118 @@
+/* Shared BubbesBlock chrome: top bar + left/right rails. Used by feed + post pages. */
+const ICON = {
+  house:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg>',
+  sugar:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18l-1.5 12.5a2 2 0 0 1-2 1.5H6.5a2 2 0 0 1-2-1.5L3 7z"/><path d="M8 7V5a4 4 0 0 1 8 0v2"/></svg>',
+  bell:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>',
+  inbox:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v16H4z"/><path d="M4 8h5l2 3h2l2-3h5"/></svg>',
+  cal:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4M16 2v4M3 9h18M5 5h14v16H5z"/></svg>',
+  history:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l9 6-9 6-9-6 9-6z"/><path d="M3 13l9 6 9-6"/></svg>',
+  candle:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2c1 1.5 1 3-0 4-1-1-1-2.5 0-4z"/><path d="M9 8h6v12H9z"/><path d="M7 20h10"/></svg>',
+  book:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5h14v14l-7-3-7 3z"/></svg>'
+};
+
+function topbar(me){
+  return `<header class="topbar">
+    <a class="brand" href="/"><span class="house">${ICON.house.replace('currentColor','#fff')}</span>BubbesBlock<small>♥ your block</small></a>
+    <div class="search">
+      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+      <input placeholder="Search your block — neighbors, favors, history…" />
+    </div>
+    <nav class="topnav">
+      <button class="navbtn active" title="Block Feed" onclick="location.href='/'">${ICON.house}</button>
+      <button class="navbtn" title="Cup of Sugar">${ICON.sugar}</button>
+      <button class="navbtn" title="Notifications"><span class="badge">5</span>${ICON.bell}</button>
+      <button class="navbtn" title="Inbox"><span class="badge">2</span>${ICON.inbox}</button>
+      <button class="avatar-btn"><span class="ava ${me.color}" style="width:100%;height:100%;border-radius:0;font-size:15px">${me.initials}</span></button>
+    </nav>
+  </header>`;
+}
+
+function leftRail(me){
+  return `<aside class="col-left">
+    <div class="me">
+      <span class="ava ${me.color}" style="width:44px;height:44px;font-size:16px">${me.initials}</span>
+      <div><b>${me.name}</b><span>${me.address}${me.verified?' · verified':''}</span></div>
+    </div>
+    <div class="menu">
+      <a class="active" href="/">${ICON.house}Block Feed</a>
+      <a href="#">${ICON.bell}Notifications<span class="pill">5</span></a>
+      <a href="#">${ICON.sugar}Cup of Sugar</a>
+      <a href="#">${ICON.cal}Block Events</a>
+      <a href="https://wholivedthere.com">${ICON.history}Who Lived Here</a>
+      <a href="#">${ICON.book}Bookmarks</a>
+    </div>
+    <div class="menu">
+      <div class="grouphd">Your Block · Beekman Ave</div>
+      <div class="grp">
+        <a href="#"><span class="dot" style="background:#3a7d5d">🥧</span> Babka &amp; Bagel Swap</a>
+        <a href="#"><span class="dot" style="background:#b8862f">🔧</span> Tools &amp; Ladders to Borrow</a>
+        <a href="#"><span class="dot" style="background:#5566b5">🐕</span> Block Dog Walkers</a>
+        <a href="#"><span class="dot" style="background:#c2557a">🕯️</span> Shabbat Dinner Club</a>
+        <a href="#"><span class="dot" style="background:#a05236">🌿</span> Holiday &amp; Sukkah Help</a>
+      </div>
+    </div>
+  </aside>`;
+}
+
+function rightRail(me){
+  return `<aside class="col-right">
+    <div class="panel claim">
+      <h3>Claim ${me.address}</h3>
+      <p class="sub">Verify you live here to post, reply &amp; unlock your home's full history.</p>
+      <a class="cta" href="https://claimmyaddress.com">Claim your address →</a>
+      <p class="fine">Free · takes 2 minutes · powered by claimmyaddress.com</p>
+    </div>
+    <div class="panel">
+      <div class="spon">Neighborhood Favorites</div>
+      <div class="biz"><span class="lg" style="background:#c2557a">🥯</span><div><b>Beekman Bagel &amp; Babka</b><span>Bakery · 2 blocks</span><div class="rate">★★★★★ <span style="color:#65676b">· 214</span></div></div></div>
+      <div class="biz"><span class="lg" style="background:#3a7d5d">🔨</span><div><b>Sunset Park Hardware</b><span>Hardware · 4 blocks</span><div class="rate">★★★★☆ <span style="color:#65676b">· 96</span></div></div></div>
+      <div class="biz"><span class="lg" style="background:#5566b5">🕯️</span><div><b>Adas Yeshurun Shul</b><span>Community · on the block</span><div class="rate">★★★★★ <span style="color:#65676b">· 58</span></div></div></div>
+    </div>
+    <div class="panel trend">
+      <h3 style="margin-bottom:10px">Trending on your block</h3>
+      <a href="/p/sukkah-help"><b>🌿 Hands needed for the sukkah</b><br><small>4 neighbors replied · 8h</small></a>
+      <a href="/p/lost-cat"><b>🐱 Found: orange tabby near 116</b><br><small>14 neighbors replied · 10h</small></a>
+      <a href="/p/block-party"><b>🎉 Sunday block party — 1pm</b><br><small>32 going · 1d</small></a>
+      <a href="/p/who-lived-here"><b>🏚️ Who lived at 104 in the '50s?</b><br><small>from wholivedthere.com · 1d</small></a>
+    </div>
+    <div class="footnote">
+      <a href="#">About</a> · <a href="#">Block Guidelines</a> · <a href="#">Privacy</a> · <a href="#">Help</a><br>
+      <a href="https://wholivedthere.com">wholivedthere.com</a> · <a href="https://claimmyaddress.com">claimmyaddress.com</a>
+      <div class="mark">BubbesBlock © 2026 — your block, the warm way ♥</div>
+    </div>
+  </aside>`;
+}
+
+/* shared post-card builders */
+function reactStack(reactions){
+  const cls=['#f3603c','#f7b500','#00874b','#5566b5'];
+  return `<div class="react">${reactions.map((r,i)=>`<span style="background:${cls[i%cls.length]}">${r}</span>`).join('')}</div>`;
+}
+function actionBar(){
+  return `<div class="actions">
+    <button class="on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/></svg>Thank</button>
+    <button><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 20l1.2-4A8.4 8.4 0 1 1 21 11.5z"/></svg>Reply</button>
+    <button><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8h16v-8"/><path d="M12 16V4"/><path d="M8 8l4-4 4 4"/></svg>Share</button>
+  </div>`;
+}
+function postHead(p){
+  return `<div class="post-head">
+    <span class="ava ${p.color}" style="width:46px;height:46px;font-size:17px">${p.initials}</span>
+    <div class="who"><b>${p.author}</b>
+      <div class="meta"><span class="gd">${p.address}</span> · ${p.time}${p.edited?' · <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Edited':''}</div>
+    </div>
+    <button class="more"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg></button>
+  </div>`;
+}
+function postBody(p){
+  return `<span class="cat ${p.catColor}">${p.category}</span>
+    <div class="body">${p.body.map(x=>`<p>${x}</p>`).join('')}</div>`;
+}
+function photoBlock(ph){
+  if(!ph) return '';
+  return `<div class="photo"><div class="ph ${ph.bg}"><span class="emoji">${ph.emoji}</span><span class="tag">${ph.tag}</span></div></div>`;
+}
+function statRow(p,count){
+  return `<div class="stats">${reactStack(p.reactions)}<span style="margin-left:8px">${p.reactedBy}</span>
+    <div class="right"><span>${count} comments</span><span>${p.shares} shares</span></div></div>`;
+}
diff --git a/public/index.html b/public/index.html
index c44f94e..ec674b1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,364 +3,45 @@
 <head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-<title>BubbesBlock — Sunset Park Block</title>
-<meta name="description" content="BubbesBlock — your neighborhood block, the warm way. Cup-of-sugar favors, block events, and the stories of who lived where." />
-<style>
-  :root{
-    --green:#00874b;          /* brand block green */
-    --green-dark:#006c3c;
-    --green-tint:#e9f6ee;
-    --plum:#7a3f6e;           /* bubbe warmth accent */
-    --ink:#1b1b1b;
-    --muted:#65676b;
-    --line:#e4e6ea;
-    --bg:#f3f1ec;             /* warm paper grey */
-    --card:#ffffff;
-    --radius:12px;
-    --shadow:0 1px 2px rgba(0,0,0,.06),0 1px 3px rgba(0,0,0,.04);
-  }
-  *{box-sizing:border-box}
-  html,body{margin:0;padding:0}
-  body{
-    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
-    background:var(--bg);color:var(--ink);font-size:15px;line-height:1.45;
-    -webkit-font-smoothing:antialiased;
-  }
-  a{color:inherit;text-decoration:none}
-  button{font-family:inherit;cursor:pointer;border:none;background:none}
-  img{display:block;max-width:100%}
-
-  /* ---------- top bar ---------- */
-  header.topbar{
-    position:sticky;top:0;z-index:50;background:var(--card);
-    border-bottom:1px solid var(--line);height:60px;display:flex;align-items:center;
-    padding:0 16px;gap:16px;
-  }
-  .brand{display:flex;align-items:center;gap:9px;font-weight:800;font-size:20px;color:var(--green);letter-spacing:-.5px}
-  .brand .house{
-    width:34px;height:34px;border-radius:10px;background:var(--green);
-    display:grid;place-items:center;flex:none;
-  }
-  .brand .house svg{width:20px;height:20px}
-  .brand small{font-weight:600;color:var(--plum);font-size:12px;margin-left:2px}
-  .search{flex:1;max-width:420px;position:relative}
-  .search input{
-    width:100%;height:40px;border-radius:22px;border:1px solid transparent;background:#eef0f2;
-    padding:0 16px 0 42px;font-size:14px;outline:none;
-  }
-  .search input:focus{background:#fff;border-color:var(--green)}
-  .search svg{position:absolute;left:14px;top:11px;width:18px;height:18px;color:var(--muted)}
-  nav.topnav{margin-left:auto;display:flex;align-items:center;gap:6px}
-  .navbtn{
-    width:44px;height:44px;border-radius:50%;display:grid;place-items:center;color:var(--muted);position:relative;
-  }
-  .navbtn:hover{background:#eef0f2;color:var(--ink)}
-  .navbtn.active{color:var(--green)}
-  .navbtn svg{width:23px;height:23px}
-  .badge{position:absolute;top:6px;right:6px;background:#e0245e;color:#fff;font-size:10px;font-weight:700;border-radius:10px;min-width:16px;height:16px;display:grid;place-items:center;padding:0 4px}
-  .avatar-btn{width:40px;height:40px;border-radius:50%;overflow:hidden;margin-left:8px;border:2px solid var(--green-tint)}
-
-  /* ---------- layout ---------- */
-  .wrap{max-width:1180px;margin:0 auto;padding:20px 16px;display:grid;grid-template-columns:248px minmax(0,1fr) 300px;gap:20px;align-items:start}
-  .col-left,.col-right{position:sticky;top:80px}
-
-  /* avatars (initials) */
-  .ava{border-radius:50%;flex:none;display:grid;place-items:center;color:#fff;font-weight:700;overflow:hidden}
-  .a1{background:#c2557a}.a2{background:#3a7d5d}.a3{background:#b8862f}.a4{background:#5566b5}.a5{background:#a05236}
-
-  /* ---------- left sidebar ---------- */
-  .me{display:flex;align-items:center;gap:11px;padding:12px;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:14px}
-  .me .ava{width:44px;height:44px}
-  .me b{display:block;font-size:15px}
-  .me span{font-size:12.5px;color:var(--muted)}
-  .menu{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:6px;margin-bottom:14px}
-  .menu a{display:flex;align-items:center;gap:13px;padding:10px 12px;border-radius:9px;font-weight:600;font-size:14.5px;color:#3a3b3c}
-  .menu a:hover{background:#f0f2f4}
-  .menu a.active{background:var(--green-tint);color:var(--green-dark)}
-  .menu a svg{width:21px;height:21px;flex:none}
-  .menu a .pill{margin-left:auto;background:var(--green);color:#fff;font-size:11px;font-weight:700;border-radius:10px;padding:1px 7px}
-  .grouphd{font-size:11px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);padding:4px 12px 8px}
-  .grp a{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:9px;font-size:14px}
-  .grp a:hover{background:#f0f2f4}
-  .grp .dot{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;flex:none}
-
-  /* ---------- center / post ---------- */
-  .backbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;font-weight:700;color:var(--muted)}
-  .backbar button{display:flex;align-items:center;gap:8px;color:var(--muted);font-weight:700;font-size:14px;padding:6px 10px;border-radius:20px}
-  .backbar button:hover{background:#e7e9ec;color:var(--ink)}
-
-  .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
-  .post{padding:16px 18px 8px}
-  .post-head{display:flex;align-items:center;gap:11px}
-  .post-head .ava{width:46px;height:46px;font-size:17px}
-  .post-head .who b{font-size:15.5px}
-  .post-head .who .meta{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px;flex-wrap:wrap}
-  .post-head .who .meta .gd{color:var(--green-dark);font-weight:600}
-  .more{margin-left:auto;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--muted)}
-  .more:hover{background:#eef0f2}
-  .cat{display:inline-flex;align-items:center;gap:6px;margin-top:13px;background:var(--green-tint);color:var(--green-dark);font-weight:700;font-size:12px;padding:5px 11px;border-radius:20px}
-  .body{font-size:16px;line-height:1.55;margin:13px 0 4px;color:#222}
-  .body p{margin:0 0 12px}
-  .photo{margin:8px -18px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
-  .photo .ph{height:330px;background:
-      radial-gradient(120% 120% at 20% 10%,#f6e7c9 0%,#e8c88c 30%,#cf9a5a 60%,#9c6b3f 100%);
-      position:relative;display:grid;place-items:center;color:#5a3c22}
-  .photo .ph .tag{position:absolute;left:14px;bottom:14px;background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:600;padding:5px 11px;border-radius:16px;backdrop-filter:blur(2px)}
-  .photo .ph .emoji{font-size:84px;filter:drop-shadow(0 6px 10px rgba(0,0,0,.18))}
-
-  .stats{display:flex;align-items:center;gap:6px;padding:12px 18px;color:var(--muted);font-size:13.5px}
-  .stats .react{display:flex;align-items:center}
-  .stats .react span{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:12px;margin-left:-6px;border:2px solid #fff}
-  .stats .react span:first-child{margin-left:0}
-  .stats .r1{background:#f3603c}.stats .r2{background:#f7b500}.stats .r3{background:var(--green)}
-  .stats .right{margin-left:auto;display:flex;gap:14px}
-
-  .actions{display:flex;border-top:1px solid var(--line);padding:4px 8px}
-  .actions button{flex:1;display:flex;align-items:center;justify-content:center;gap:9px;padding:10px;border-radius:9px;color:var(--muted);font-weight:700;font-size:14.5px}
-  .actions button:hover{background:#f0f2f4}
-  .actions button.on{color:var(--green)}
-  .actions svg{width:21px;height:21px}
-
-  /* comments */
-  .comments{padding:6px 18px 16px}
-  .compose{display:flex;gap:10px;align-items:center;padding:10px 0 6px}
-  .compose .ava{width:36px;height:36px;font-size:14px}
-  .compose .field{flex:1;display:flex;align-items:center;background:#eef0f2;border-radius:22px;padding:0 8px 0 16px;height:42px}
-  .compose .field input{flex:1;border:none;background:none;outline:none;font-size:14px;height:100%}
-  .compose .field button{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;color:var(--green)}
-  .compose .field button:hover{background:#dfe3e8}
-
-  .cmt{display:flex;gap:10px;padding:10px 0}
-  .cmt .ava{width:36px;height:36px;font-size:14px}
-  .cmt .bub{background:#f0f2f4;border-radius:16px;padding:9px 13px;display:inline-block}
-  .cmt .bub b{font-size:14px}
-  .cmt .bub .nb{font-size:12px;color:var(--muted);font-weight:600}
-  .cmt .bub p{margin:2px 0 0;font-size:14.5px}
-  .cmt .meta{display:flex;gap:16px;font-size:12.5px;font-weight:700;color:var(--muted);padding:5px 0 0 13px}
-  .cmt .meta a:hover{color:var(--green);text-decoration:underline}
-  .cmt.reply{margin-left:46px}
-  .morecmt{padding:8px 0 0 0;font-weight:700;color:var(--muted);font-size:13.5px}
-  .morecmt:hover{color:var(--green);cursor:pointer}
-
-  /* ---------- right sidebar ---------- */
-  .panel{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:15px;margin-bottom:16px}
-  .panel h3{margin:0 0 4px;font-size:15px}
-  .panel .sub{font-size:13px;color:var(--muted);margin:0 0 12px}
-  .claim{background:linear-gradient(150deg,var(--green) 0%,var(--green-dark) 100%);color:#fff;border:none}
-  .claim h3{color:#fff}.claim .sub{color:rgba(255,255,255,.85)}
-  .claim .cta{display:block;text-align:center;background:#fff;color:var(--green-dark);font-weight:800;padding:10px;border-radius:22px;font-size:14px;margin-top:4px}
-  .claim .fine{font-size:11px;color:rgba(255,255,255,.8);text-align:center;margin:9px 0 0}
-  .spon{font-size:10.5px;font-weight:800;letter-spacing:.6px;color:var(--muted);text-transform:uppercase;margin-bottom:8px}
-  .biz{display:flex;gap:11px;align-items:center;padding:8px 0;border-top:1px solid var(--line)}
-  .biz:first-of-type{border-top:none}
-  .biz .lg{width:42px;height:42px;border-radius:10px;display:grid;place-items:center;color:#fff;font-size:18px;flex:none}
-  .biz b{font-size:14px;display:block}
-  .biz span{font-size:12px;color:var(--muted)}
-  .biz .rate{color:#e8a200;font-size:12px}
-  .trend a{display:block;padding:8px 0;border-top:1px solid var(--line);font-size:14px}
-  .trend a:first-of-type{border-top:none}
-  .trend a:hover b{color:var(--green-dark)}
-  .trend small{color:var(--muted);font-size:12px}
-  .footnote{font-size:12px;color:var(--muted);padding:0 6px;line-height:1.7}
-  .footnote a:hover{text-decoration:underline}
-  .footnote .mark{margin-top:8px;font-weight:700;color:var(--green)}
-
-  @media(max-width:980px){
-    .wrap{grid-template-columns:minmax(0,1fr) 300px}
-    .col-left{display:none}
-  }
-  @media(max-width:760px){
-    .wrap{grid-template-columns:1fr}
-    .col-right{display:none}
-    .search{display:none}
-    .photo .ph{height:240px}
-  }
-</style>
+<title>BubbesBlock — Beekman Ave</title>
+<meta name="description" content="BubbesBlock — your neighborhood block, the warm way. Cup-of-sugar favors, block events, Shabbat dinners, and the stories of who lived where." />
+<link rel="stylesheet" href="/styles.css" />
 </head>
 <body>
-
-<!-- ===================== TOP BAR ===================== -->
-<header class="topbar">
-  <a class="brand" href="/">
-    <span class="house">
-      <svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/><path d="M9 20v-6h6v6"/></svg>
-    </span>
-    BubbesBlock<small>♥ your block</small>
-  </a>
-
-  <div class="search">
-    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
-    <input placeholder="Search your block — neighbors, favors, history…" />
-  </div>
-
-  <nav class="topnav">
-    <button class="navbtn active" title="Block Feed"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg></button>
-    <button class="navbtn" title="Cup of Sugar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18l-1.5 12.5a2 2 0 0 1-2 1.5H6.5a2 2 0 0 1-2-1.5L3 7z"/><path d="M8 7V5a4 4 0 0 1 8 0v2"/></svg></button>
-    <button class="navbtn" title="Notifications"><span class="badge">5</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></button>
-    <button class="navbtn" title="Inbox"><span class="badge">2</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16v16H4z"/><path d="M4 8h5l2 3h2l2-3h5"/></svg></button>
-    <button class="avatar-btn"><span class="ava a1" style="width:100%;height:100%;border-radius:0;font-size:15px">MS</span></button>
-  </nav>
-</header>
-
-<!-- ===================== BODY ===================== -->
+<div id="hdr"></div>
 <div class="wrap">
+  <div id="left"></div>
+  <main class="col-center" id="feed"></main>
+  <div id="right"></div>
+</div>
 
-  <!-- ---------- LEFT ---------- -->
-  <aside class="col-left">
-    <div class="me">
-      <span class="ava a1" style="width:44px;height:44px;font-size:16px">MS</span>
-      <div><b>Miriam Stein</b><span>112 Beekman Ave · verified</span></div>
-    </div>
-
-    <div class="menu">
-      <a class="active" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg>Block Feed</a>
-      <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>Notifications<span class="pill">5</span></a>
-      <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18l-1.5 12.5a2 2 0 0 1-2 1.5H6.5a2 2 0 0 1-2-1.5z"/><path d="M8 7V5a4 4 0 0 1 8 0v2"/></svg>Cup of Sugar</a>
-      <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4M16 2v4M3 9h18M5 5h14v16H5z"/></svg>Block Events</a>
-      <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l9 6-9 6-9-6 9-6z"/><path d="M3 13l9 6 9-6"/></svg>Who Lived Here</a>
-      <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5h14v14l-7-3-7 3z"/></svg>Bookmarks</a>
-    </div>
-
-    <div class="menu">
-      <div class="grouphd">Your Block · Beekman Ave</div>
-      <div class="grp">
-        <a href="#"><span class="dot" style="background:#3a7d5d">🥧</span> Babka & Bagel Swap</a>
-        <a href="#"><span class="dot" style="background:#b8862f">🔧</span> Tools &amp; Ladders to Borrow</a>
-        <a href="#"><span class="dot" style="background:#5566b5">🐕</span> Block Dog Walkers</a>
-        <a href="#"><span class="dot" style="background:#c2557a">🕯️</span> Shabbat Dinner Club</a>
-      </div>
-    </div>
-  </aside>
+<script src="/chrome.js"></script>
+<script>
+fetch('/api/posts').then(r=>r.json()).then(d=>{
+  document.getElementById('hdr').innerHTML   = topbar(d.me);
+  document.getElementById('left').innerHTML  = leftRail(d.me);
+  document.getElementById('right').innerHTML = rightRail(d.me);
 
-  <!-- ---------- CENTER ---------- -->
-  <main class="col-center">
-    <div class="backbar">
-      <button>
-        <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
-        Back to Block Feed
-      </button>
-    </div>
+  const newpost = `<div class="newpost">
+    <span class="ava ${d.me.color}" style="width:42px;height:42px;font-size:15px">${d.me.initials}</span>
+    <div class="fake">Share something with the block, ${d.me.name.split(' ')[0]}…</div>
+  </div>`;
 
-    <article class="card">
+  const cards = d.posts.map(p=>`
+    <article class="card feedcard" onclick="location.href='/p/${p.id}'">
       <div class="post">
-        <div class="post-head">
-          <span class="ava a3" style="width:46px;height:46px;font-size:17px">RB</span>
-          <div class="who">
-            <b>Bubbe Ruth Bernstein</b>
-            <div class="meta"><span class="gd">108 Beekman Ave</span> · 3h · <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Edited</div>
-          </div>
-          <button class="more"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg></button>
-        </div>
-
-        <span class="cat">🥧 Cup of Sugar · Free to a neighbor</span>
-
-        <div class="body">
-          <p><b>Made too much honey cake again.</b> 🍯 I always tell myself "Ruth, half a recipe!" and then I make a recipe and a half. So — there are <b>three loaves</b> on my stoop at 108 Beekman, wrapped and labeled, free for whoever wants one.</p>
-          <p>Take one, leave the foil for the next bubbe. And if anyone has a folding table I can borrow for Sunday's block party, I'll trade you a loaf for it. Knock anytime, I'm home. ♥</p>
-        </div>
-      </div>
-
-      <div class="photo">
-        <div class="ph">
-          <span class="emoji">🍯🍰</span>
-          <span class="tag">📍 108 Beekman Ave · the famous stoop</span>
-        </div>
-      </div>
-
-      <div class="stats">
-        <div class="react">
-          <span class="r1">❤️</span><span class="r2">😋</span><span class="r3">👍</span>
-        </div>
-        <span style="margin-left:8px">Goldie, Sol &amp; 41 neighbors</span>
-        <div class="right"><span>18 comments</span><span>6 shares</span></div>
-      </div>
-
-      <div class="actions">
-        <button class="on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/></svg>Thank</button>
-        <button><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 20l1.2-4A8.4 8.4 0 1 1 21 11.5z"/></svg>Reply</button>
-        <button><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8h16v-8"/><path d="M12 16V4"/><path d="M8 8l4-4 4 4"/></svg>Share</button>
-      </div>
-
-      <!-- comments -->
-      <div class="comments">
-        <div class="compose">
-          <span class="ava a1" style="width:36px;height:36px;font-size:13px">MS</span>
-          <div class="field">
-            <input placeholder="Add a neighborly reply…" />
-            <button title="Photo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M21 17l-5-5-9 7"/></svg></button>
-            <button title="Send"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg></button>
-          </div>
-        </div>
-
-        <div class="cmt">
-          <span class="ava a2" style="width:36px;height:36px;font-size:14px">GF</span>
-          <div>
-            <div class="bub"><b>Goldie Fein</b> <span class="nb">· 114 Beekman</span><p>You are a treasure, Ruth. I'll send my grandson over for a loaf — and he'll bring the folding table back to you Saturday, promise. 🙏</p></div>
-            <div class="meta"><span>2h</span><a>Thank · 12</a><a>Reply</a></div>
-          </div>
-        </div>
-
-        <div class="cmt reply">
-          <span class="ava a3" style="width:36px;height:36px;font-size:13px">RB</span>
-          <div>
-            <div class="bub"><b>Bubbe Ruth Bernstein</b> <span class="nb">· author</span><p>Deal! Tell him there's an extra slice in it for him. 😉</p></div>
-            <div class="meta"><span>2h</span><a>Thank · 8</a><a>Reply</a></div>
-          </div>
-        </div>
-
-        <div class="cmt">
-          <span class="ava a4" style="width:36px;height:36px;font-size:14px">SK</span>
-          <div>
-            <div class="bub"><b>Sol Katz</b> <span class="nb">· 121 Beekman</span><p>This block has the best stoop in Sunset Park, hands down. Saving one for Mrs. Adler at 110, she doesn't get out much these days.</p></div>
-            <div class="meta"><span>1h</span><a>Thank · 23</a><a>Reply</a></div>
-          </div>
-        </div>
-
-        <div class="morecmt">View 15 more comments</div>
-      </div>
-    </article>
-  </main>
-
-  <!-- ---------- RIGHT ---------- -->
-  <aside class="col-right">
-    <div class="panel claim">
-      <h3>Claim 112 Beekman Ave</h3>
-      <p class="sub">Verify you live here to post, reply &amp; unlock your home's full history.</p>
-      <a class="cta" href="https://claimmyaddress.com">Claim your address →</a>
-      <p class="fine">Free · takes 2 minutes · powered by claimmyaddress.com</p>
-    </div>
-
-    <div class="panel">
-      <div class="spon">Neighborhood Favorites</div>
-      <div class="biz">
-        <span class="lg" style="background:#c2557a">🥯</span>
-        <div><b>Beekman Bagel &amp; Babka</b><span>Bakery · 2 blocks</span><div class="rate">★★★★★ <span style="color:#65676b">· 214</span></div></div>
-      </div>
-      <div class="biz">
-        <span class="lg" style="background:#3a7d5d">🔨</span>
-        <div><b>Sunset Park Hardware</b><span>Hardware · 4 blocks</span><div class="rate">★★★★☆ <span style="color:#65676b">· 96</span></div></div>
-      </div>
-      <div class="biz">
-        <span class="lg" style="background:#5566b5">🕯️</span>
-        <div><b>Adas Yeshurun Shul</b><span>Community · on the block</span><div class="rate">★★★★★ <span style="color:#65676b">· 58</span></div></div>
-      </div>
-    </div>
-
-    <div class="panel trend">
-      <h3 style="margin-bottom:10px">Trending on your block</h3>
-      <a href="#"><b>🔧 Who has a tall ladder?</b><br><small>9 neighbors replied · 1h</small></a>
-      <a href="#"><b>🐱 Found: orange tabby near 116</b><br><small>14 neighbors replied · 3h</small></a>
-      <a href="#"><b>🎉 Sunday block party — 1pm</b><br><small>32 going · 5h</small></a>
-      <a href="#"><b>🏚️ Who lived at 104 in the '60s?</b><br><small>from wholivedthere.com · 1d</small></a>
-    </div>
-
-    <div class="footnote">
-      <a href="#">About</a> · <a href="#">Block Guidelines</a> · <a href="#">Privacy</a> · <a href="#">Help</a><br>
-      <a href="https://wholivedthere.com">wholivedthere.com</a> · <a href="https://claimmyaddress.com">claimmyaddress.com</a>
-      <div class="mark">BubbesBlock © 2026 — your block, the warm way ♥</div>
-    </div>
-  </aside>
-
-</div>
+        ${postHead(p)}
+        ${postBody(p)}
+      </div>
+      ${photoBlock(p.photo)}
+      ${statRow(p, p.commentCount)}
+      ${actionBar()}
+    </article>`).join('');
+
+  document.getElementById('feed').innerHTML = newpost + cards;
+}).catch(e=>{
+  document.getElementById('feed').innerHTML = '<div class="card"><div class="post">Could not load the block feed.</div></div>';
+});
+</script>
 </body>
 </html>
diff --git a/public/post.html b/public/post.html
new file mode 100644
index 0000000..0981cdf
--- /dev/null
+++ b/public/post.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>BubbesBlock — Post</title>
+<link rel="stylesheet" href="/styles.css" />
+</head>
+<body>
+<div id="hdr"></div>
+<div class="wrap">
+  <div id="left"></div>
+  <main class="col-center" id="main"></main>
+  <div id="right"></div>
+</div>
+
+<script src="/chrome.js"></script>
+<script>
+// id from /p/:id path, or ?id= fallback
+const parts = location.pathname.split('/').filter(Boolean);
+const id = (parts[0]==='p' && parts[1]) ? parts[1] : new URLSearchParams(location.search).get('id');
+
+function comment(c){
+  return `<div class="cmt${c.reply?' reply':''}">
+    <span class="ava ${c.color}" style="width:36px;height:36px;font-size:${c.reply?13:14}px">${c.initials}</span>
+    <div>
+      <div class="bub"><b>${c.author}</b> <span class="nb">· ${c.nb}</span><p>${c.body}</p></div>
+      <div class="meta"><span>${c.time}</span><a>Thank · ${c.thanks}</a><a>Reply</a></div>
+    </div>
+  </div>`;
+}
+
+fetch('/api/posts/'+encodeURIComponent(id)).then(r=>{
+  if(!r.ok) throw new Error('not found');
+  return r.json();
+}).then(d=>{
+  const p = d.post;
+  document.title = `BubbesBlock — ${p.author}`;
+  document.getElementById('hdr').innerHTML   = topbar(d.me);
+  document.getElementById('left').innerHTML  = leftRail(d.me);
+  document.getElementById('right').innerHTML = rightRail(d.me);
+
+  const cmts = (p.comments||[]).map(comment).join('');
+  const more = p.moreComments ? `<div class="morecmt">View ${p.moreComments} more comments</div>` : '';
+
+  document.getElementById('main').innerHTML = `
+    <div class="backbar"><a href="/">
+      <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
+      Back to Block Feed</a></div>
+    <article class="card">
+      <div class="post">${postHead(p)}${postBody(p)}</div>
+      ${photoBlock(p.photo)}
+      ${statRow(p, (p.comments?p.comments.length:0)+(p.moreComments||0))}
+      ${actionBar()}
+      <div class="comments">
+        <div class="compose">
+          <span class="ava ${d.me.color}" style="width:36px;height:36px;font-size:13px">${d.me.initials}</span>
+          <div class="field">
+            <input placeholder="Add a neighborly reply…" />
+            <button title="Photo"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M21 17l-5-5-9 7"/></svg></button>
+            <button title="Send"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg></button>
+          </div>
+        </div>
+        ${cmts}${more}
+      </div>
+    </article>`;
+}).catch(()=>{
+  document.getElementById('hdr').innerHTML = '';
+  document.getElementById('main').innerHTML = '<div class="card"><div class="post"><b>Post not found.</b><br><a href="/" style="color:var(--green-dark);font-weight:700">← Back to the block feed</a></div></div>';
+});
+</script>
+</body>
+</html>
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000..247cb1f
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,150 @@
+:root{
+  --green:#00874b; --green-dark:#006c3c; --green-tint:#e9f6ee;
+  --plum:#7a3f6e; --plum-tint:#f4eaf2; --amber:#b8862f; --amber-tint:#f8efdb;
+  --ink:#1b1b1b; --muted:#65676b; --line:#e4e6ea; --bg:#f3f1ec; --card:#ffffff;
+  --radius:12px; --shadow:0 1px 2px rgba(0,0,0,.06),0 1px 3px rgba(0,0,0,.04);
+}
+*{box-sizing:border-box}
+html,body{margin:0;padding:0}
+body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
+  background:var(--bg);color:var(--ink);font-size:15px;line-height:1.45;-webkit-font-smoothing:antialiased}
+a{color:inherit;text-decoration:none}
+button{font-family:inherit;cursor:pointer;border:none;background:none}
+img{display:block;max-width:100%}
+
+/* top bar */
+header.topbar{position:sticky;top:0;z-index:50;background:var(--card);border-bottom:1px solid var(--line);
+  height:60px;display:flex;align-items:center;padding:0 16px;gap:16px}
+.brand{display:flex;align-items:center;gap:9px;font-weight:800;font-size:20px;color:var(--green);letter-spacing:-.5px}
+.brand .house{width:34px;height:34px;border-radius:10px;background:var(--green);display:grid;place-items:center;flex:none}
+.brand .house svg{width:20px;height:20px}
+.brand small{font-weight:600;color:var(--plum);font-size:12px;margin-left:2px}
+.search{flex:1;max-width:420px;position:relative}
+.search input{width:100%;height:40px;border-radius:22px;border:1px solid transparent;background:#eef0f2;
+  padding:0 16px 0 42px;font-size:14px;outline:none}
+.search input:focus{background:#fff;border-color:var(--green)}
+.search svg{position:absolute;left:14px;top:11px;width:18px;height:18px;color:var(--muted)}
+nav.topnav{margin-left:auto;display:flex;align-items:center;gap:6px}
+.navbtn{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;color:var(--muted);position:relative}
+.navbtn:hover{background:#eef0f2;color:var(--ink)}
+.navbtn.active{color:var(--green)}
+.navbtn svg{width:23px;height:23px}
+.badge{position:absolute;top:6px;right:6px;background:#e0245e;color:#fff;font-size:10px;font-weight:700;
+  border-radius:10px;min-width:16px;height:16px;display:grid;place-items:center;padding:0 4px}
+.avatar-btn{width:40px;height:40px;border-radius:50%;overflow:hidden;margin-left:8px;border:2px solid var(--green-tint)}
+
+/* layout */
+.wrap{max-width:1180px;margin:0 auto;padding:20px 16px;display:grid;grid-template-columns:248px minmax(0,1fr) 300px;gap:20px;align-items:start}
+.col-left,.col-right{position:sticky;top:80px}
+
+/* avatars */
+.ava{border-radius:50%;flex:none;display:grid;place-items:center;color:#fff;font-weight:700;overflow:hidden}
+.a1{background:#c2557a}.a2{background:#3a7d5d}.a3{background:#b8862f}.a4{background:#5566b5}.a5{background:#a05236}
+
+/* left sidebar */
+.me{display:flex;align-items:center;gap:11px;padding:12px;background:var(--card);border:1px solid var(--line);
+  border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:14px}
+.me .ava{width:44px;height:44px}
+.me b{display:block;font-size:15px}.me span{font-size:12.5px;color:var(--muted)}
+.menu{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:6px;margin-bottom:14px}
+.menu a{display:flex;align-items:center;gap:13px;padding:10px 12px;border-radius:9px;font-weight:600;font-size:14.5px;color:#3a3b3c}
+.menu a:hover{background:#f0f2f4}
+.menu a.active{background:var(--green-tint);color:var(--green-dark)}
+.menu a svg{width:21px;height:21px;flex:none}
+.menu a .pill{margin-left:auto;background:var(--green);color:#fff;font-size:11px;font-weight:700;border-radius:10px;padding:1px 7px}
+.grouphd{font-size:11px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:var(--muted);padding:4px 12px 8px}
+.grp a{display:flex;align-items:center;gap:11px;padding:8px 12px;border-radius:9px;font-size:14px}
+.grp a:hover{background:#f0f2f4}
+.grp .dot{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;color:#fff;font-size:14px;flex:none}
+
+/* compose box (feed top) */
+.newpost{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:14px 16px;margin-bottom:16px;display:flex;gap:11px;align-items:center}
+.newpost .ava{width:42px;height:42px;font-size:15px}
+.newpost .fake{flex:1;height:42px;border-radius:22px;background:#eef0f2;display:flex;align-items:center;padding:0 18px;color:var(--muted);font-size:15px}
+.newpost .fake:hover{background:#e7e9ec;cursor:pointer}
+
+/* post card */
+.backbar{display:flex;align-items:center;gap:10px;margin-bottom:12px}
+.backbar a{display:flex;align-items:center;gap:8px;color:var(--muted);font-weight:700;font-size:14px;padding:6px 10px;border-radius:20px}
+.backbar a:hover{background:#e7e9ec;color:var(--ink)}
+.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:16px;overflow:hidden}
+.post{padding:16px 18px 8px}
+.post-head{display:flex;align-items:center;gap:11px}
+.post-head .ava{width:46px;height:46px;font-size:17px}
+.post-head .who b{font-size:15.5px}
+.post-head .who .meta{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px;flex-wrap:wrap}
+.post-head .who .meta .gd{color:var(--green-dark);font-weight:600}
+.more{margin-left:auto;width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--muted)}
+.more:hover{background:#eef0f2}
+.cat{display:inline-flex;align-items:center;gap:6px;margin-top:13px;font-weight:700;font-size:12px;padding:5px 11px;border-radius:20px}
+.cat.green{background:var(--green-tint);color:var(--green-dark)}
+.cat.plum{background:var(--plum-tint);color:var(--plum)}
+.cat.amber{background:var(--amber-tint);color:var(--amber)}
+.body{font-size:16px;line-height:1.55;margin:13px 0 4px;color:#222}
+.body p{margin:0 0 12px}
+.photo{margin:8px 0 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
+.photo .ph{height:330px;position:relative;display:grid;place-items:center}
+.ph.warm{background:radial-gradient(120% 120% at 20% 10%,#f6e7c9,#e8c88c 30%,#cf9a5a 60%,#9c6b3f)}
+.ph.candle{background:radial-gradient(110% 120% at 50% 12%,#fff3cf,#f0c873 28%,#b9743a 65%,#5e2f1f)}
+.ph.green{background:radial-gradient(120% 120% at 25% 12%,#e7f3d4,#bcd98a 35%,#6f9e4a 70%,#3d6128)}
+.ph.amber{background:radial-gradient(120% 120% at 22% 10%,#fbe7c2,#eab95f 40%,#c07f2c 75%,#7c4d15)}
+.ph.sepia{background:radial-gradient(120% 120% at 30% 14%,#efe2cb,#cdb38a 40%,#9c7c53 72%,#5e482c)}
+.photo .ph .tag{position:absolute;left:14px;bottom:14px;background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:600;padding:5px 11px;border-radius:16px;backdrop-filter:blur(2px)}
+.photo .ph .emoji{font-size:84px;filter:drop-shadow(0 6px 10px rgba(0,0,0,.18))}
+.stats{display:flex;align-items:center;gap:6px;padding:12px 18px;color:var(--muted);font-size:13.5px}
+.stats .react{display:flex;align-items:center}
+.stats .react span{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:12px;margin-left:-6px;border:2px solid #fff;background:#eee}
+.stats .react span:first-child{margin-left:0}
+.stats .right{margin-left:auto;display:flex;gap:14px}
+.actions{display:flex;border-top:1px solid var(--line);padding:4px 8px}
+.actions button{flex:1;display:flex;align-items:center;justify-content:center;gap:9px;padding:10px;border-radius:9px;color:var(--muted);font-weight:700;font-size:14.5px}
+.actions button:hover{background:#f0f2f4}
+.actions button.on{color:var(--green)}
+.actions svg{width:21px;height:21px}
+
+/* comments */
+.comments{padding:6px 18px 16px}
+.compose{display:flex;gap:10px;align-items:center;padding:10px 0 6px}
+.compose .ava{width:36px;height:36px;font-size:14px}
+.compose .field{flex:1;display:flex;align-items:center;background:#eef0f2;border-radius:22px;padding:0 8px 0 16px;height:42px}
+.compose .field input{flex:1;border:none;background:none;outline:none;font-size:14px;height:100%}
+.compose .field button{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;color:var(--green)}
+.compose .field button:hover{background:#dfe3e8}
+.cmt{display:flex;gap:10px;padding:10px 0}
+.cmt .ava{width:36px;height:36px;font-size:14px}
+.cmt .bub{background:#f0f2f4;border-radius:16px;padding:9px 13px;display:inline-block}
+.cmt .bub b{font-size:14px}
+.cmt .bub .nb{font-size:12px;color:var(--muted);font-weight:600}
+.cmt .bub p{margin:2px 0 0;font-size:14.5px}
+.cmt .meta{display:flex;gap:16px;font-size:12.5px;font-weight:700;color:var(--muted);padding:5px 0 0 13px}
+.cmt .meta a:hover{color:var(--green);text-decoration:underline;cursor:pointer}
+.cmt.reply{margin-left:46px}
+.morecmt{padding:8px 0 0;font-weight:700;color:var(--muted);font-size:13.5px}
+.morecmt:hover{color:var(--green);cursor:pointer}
+
+/* right sidebar */
+.panel{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:15px;margin-bottom:16px}
+.panel h3{margin:0 0 4px;font-size:15px}
+.panel .sub{font-size:13px;color:var(--muted);margin:0 0 12px}
+.claim{background:linear-gradient(150deg,var(--green),var(--green-dark));color:#fff;border:none}
+.claim h3{color:#fff}.claim .sub{color:rgba(255,255,255,.85)}
+.claim .cta{display:block;text-align:center;background:#fff;color:var(--green-dark);font-weight:800;padding:10px;border-radius:22px;font-size:14px;margin-top:4px}
+.claim .fine{font-size:11px;color:rgba(255,255,255,.8);text-align:center;margin:9px 0 0}
+.spon{font-size:10.5px;font-weight:800;letter-spacing:.6px;color:var(--muted);text-transform:uppercase;margin-bottom:8px}
+.biz{display:flex;gap:11px;align-items:center;padding:8px 0;border-top:1px solid var(--line)}
+.biz:first-of-type{border-top:none}
+.biz .lg{width:42px;height:42px;border-radius:10px;display:grid;place-items:center;color:#fff;font-size:18px;flex:none}
+.biz b{font-size:14px;display:block}.biz span{font-size:12px;color:var(--muted)}
+.biz .rate{color:#e8a200;font-size:12px}
+.trend a{display:block;padding:8px 0;border-top:1px solid var(--line);font-size:14px}
+.trend a:first-of-type{border-top:none}
+.trend a:hover b{color:var(--green-dark)}
+.trend small{color:var(--muted);font-size:12px}
+.footnote{font-size:12px;color:var(--muted);padding:0 6px;line-height:1.7}
+.footnote a:hover{text-decoration:underline}
+.footnote .mark{margin-top:8px;font-weight:700;color:var(--green)}
+.feedcard{cursor:pointer}
+.feedcard:hover{box-shadow:0 2px 10px rgba(0,0,0,.10)}
+
+@media(max-width:980px){.wrap{grid-template-columns:minmax(0,1fr) 300px}.col-left{display:none}}
+@media(max-width:760px){.wrap{grid-template-columns:1fr}.col-right{display:none}.search{display:none}.photo .ph{height:240px}}
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..5c99cb8
--- /dev/null
+++ b/server.js
@@ -0,0 +1,46 @@
+'use strict';
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+
+const app = express();
+const PORT = process.env.PORT || 3201;
+const DATA = path.join(__dirname, 'data', 'posts.json');
+
+app.use(express.json());
+app.use(express.static(path.join(__dirname, 'public')));
+
+// Read fresh each request so seed edits show up without a restart.
+function load() {
+  return JSON.parse(fs.readFileSync(DATA, 'utf8'));
+}
+
+// Feed: lightweight list (no comment bodies) for the home page.
+app.get('/api/posts', (req, res) => {
+  try {
+    const d = load();
+    const feed = d.posts.map(p => ({
+      id: p.id, author: p.author, initials: p.initials, color: p.color,
+      address: p.address, time: p.time, edited: !!p.edited,
+      category: p.category, catColor: p.catColor, body: p.body,
+      photo: p.photo, reactions: p.reactions, reactedBy: p.reactedBy,
+      shares: p.shares, commentCount: (p.comments ? p.comments.length : 0) + (p.moreComments || 0)
+    }));
+    res.json({ neighborhood: d.neighborhood, me: d.me, posts: feed });
+  } catch (e) { res.status(500).json({ error: String(e) }); }
+});
+
+// Single post with full comment thread for the detail page.
+app.get('/api/posts/:id', (req, res) => {
+  try {
+    const d = load();
+    const post = d.posts.find(p => p.id === req.params.id);
+    if (!post) return res.status(404).json({ error: 'not found' });
+    res.json({ neighborhood: d.neighborhood, me: d.me, post });
+  } catch (e) { res.status(500).json({ error: String(e) }); }
+});
+
+// Pretty post URL -> serve the detail page (client reads ?id or the path).
+app.get('/p/:id', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'post.html')));
+
+app.listen(PORT, () => console.log(`BubbesBlock running on http://localhost:${PORT}`));

← 5be1261 Add Nextdoor-style block post-detail UI (public/index.html)  ·  back to Bubbesblock  ·  Add /healthz route + deploy config; live on bubbesblock.com 18d989b →