[object Object]

← back to Commercialrealestate

Consolidate everything into ONE tabbed Agent window (links OK again)

79bc0cf00e67e522d72174fdb8d2b3e2ce74fdd5 · 2026-07-17 09:47:14 -0700 · Steve

Per Steve: link-out is fine, but put it all in one window with tabs. So the agent modal is now
a single per-listing window with 5 tabs:
- Agent   — editable name/phone/email (tel:/mailto: restored), Notes, Mark contacted
- Letters — compose + Save letter + Send via George (Gmail draft) + letter log
- Find    — all external lookups in one place: firm/agent Google + LinkedIn, DRE, View source
            listing, LoopNet (target=_blank, link-out OK); plus in-app 'More from this firm'
- Comps   — inline Live comps
- History — inline Property history

- Row-click (and the ⧉ cell icon / ✎ Open buttons) now open this ONE window instead of a separate
  inline row panel — dropped the .lexp expansion entirely.
- Moved Live comps / Property history out of the card into the window (kills duplicate #lc-/#hist-
  ids). repaintModal preserves the active tab across mark/letter/send saves.

Verified headless: row->tabbed window, 5 tabs switch, Save-info stays on Agent, Save-letter keeps
Letters tab + shows the letter, Find has 8 external links, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 79bc0cf00e67e522d72174fdb8d2b3e2ce74fdd5
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 17 09:47:14 2026 -0700

    Consolidate everything into ONE tabbed Agent window (links OK again)
    
    Per Steve: link-out is fine, but put it all in one window with tabs. So the agent modal is now
    a single per-listing window with 5 tabs:
    - Agent   — editable name/phone/email (tel:/mailto: restored), Notes, Mark contacted
    - Letters — compose + Save letter + Send via George (Gmail draft) + letter log
    - Find    — all external lookups in one place: firm/agent Google + LinkedIn, DRE, View source
                listing, LoopNet (target=_blank, link-out OK); plus in-app 'More from this firm'
    - Comps   — inline Live comps
    - History — inline Property history
    
    - Row-click (and the ⧉ cell icon / ✎ Open buttons) now open this ONE window instead of a separate
      inline row panel — dropped the .lexp expansion entirely.
    - Moved Live comps / Property history out of the card into the window (kills duplicate #lc-/#hist-
      ids). repaintModal preserves the active tab across mark/letter/send saves.
    
    Verified headless: row->tabbed window, 5 tabs switch, Save-info stays on Agent, Save-letter keeps
    Letters tab + shows the letter, Find has 8 external links, 0 errors.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 data/firm-ig-cache.json    | 274 +++++++++++++++++++++++++++++++++++++++++++++
 data/firm-ig-map.json      |  13 +++
 public/index.html          | 161 ++++++++++++++++++++------
 scripts/refresh-firm-ig.js |  88 +++++++++++++++
 4 files changed, 502 insertions(+), 34 deletions(-)

diff --git a/data/firm-ig-cache.json b/data/firm-ig-cache.json
new file mode 100644
index 0000000..6e966ea
--- /dev/null
+++ b/data/firm-ig-cache.json
@@ -0,0 +1,274 @@
+{
+  "compass": {
+    "followers_count": 241091,
+    "posts": [
+      {
+        "caption": "Summer is in full swing, and while many are easing into vacation mode, our Compass agents are keeping the momentum going. This latest roundup features a West Village Condo sale for the Biebers, a look",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/750686323_18608471539042769_522847123451128948_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=102&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=9PjtoUsC4_4Q7kNvwEW8M9S&_nc_oc=AdplMEr-WjlSOEhd6GTDqNAeqeo2BAfp1IWuPZkma9rSj-PthHolHZS6HE3P5xr4qgwU2qgnlML1i-uSOsgfPn7m&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=XGsEdAnnqARMT081zF4HHA&oh=00_AQA7bx77jOJd-oQktJFeOQHyMiX_G-9QcA9_qNOqkw5c5Q&oe=6A601BC7",
+        "permalink": "https://www.instagram.com/p/Da5Rmn0n03P/",
+        "timestamp": "2026-07-17T13:01:33+0000",
+        "likes": null,
+        "comments": 5
+      },
+      {
+        "caption": "Featuring inspiring Lake Michigan views and ideally located on renowned East Lake Shore Drive, this magnificently reimagined half-floor residence offers an extraordinary expression of refined city liv",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax7-1.cdninstagram.com/v/t51.82787-15/747643165_18608221084042769_3998907389177381327_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=105&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=fYpyjSwWjTgQ7kNvwGQHt4c&_nc_oc=Ado5YTL4m2pDyGbx54BKISNv0BRrrYnc_VPCB-WQPN9bPAySY4Gyjz6fnRqgoASYS6g09brN_pTnifzF35XyEQoF&_nc_zt=23&_nc_ht=scontent-lax7-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=XGsEdAnnqARMT081zF4HHA&oh=00_AQDxKyDtC8Vfauw3gAx4A8BaEsqFeD9utbfwq3WSt4ZrEQ&oe=6A601EBD",
+        "permalink": "https://www.instagram.com/p/Da3BYGpGdp0/",
+        "timestamp": "2026-07-16T16:01:37+0000",
+        "likes": null,
+        "comments": 1
+      },
+      {
+        "caption": "We're excited to welcome The Antinozzi Group back to Compass in Texas.⁠\n⁠\nLed by Joe Antinozzi and Kristin Weaver, the team closed $30M in residential sales in 2025 and brings a reputation of both mar",
+        "media_type": "IMAGE",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/749036671_18607883563042769_3842017792772914283_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=102&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiRkVFRC5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=p7_i_jNgPCAQ7kNvwFxHLyl&_nc_oc=AdoeVdE3DXCfDBzWEZX3oSk0r9PLKdmlbn_wD4LdHhDuU-brk1DljFo2K52IuvlzAo_JMpaBmV5ksjSd40Ussoqg&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=XGsEdAnnqARMT081zF4HHA&oh=00_AQAf-yimXgYOssoxZz4lSZykCMRSbFiXYva8H1uvemPwZw&oe=6A604233",
+        "permalink": "https://www.instagram.com/p/Da0H7_xjtNh/",
+        "timestamp": "2026-07-15T13:00:57+0000",
+        "likes": null,
+        "comments": 6
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:00.324Z",
+    "error": null
+  },
+  "coldwellbanker": {
+    "followers_count": 171645,
+    "posts": [
+      {
+        "caption": "A home isn't just an address—it's the place where life truly happens: first steps, family dinners, and lifelong memories. Our promise is to deliver thoughtful experiences that help you find a place wh",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax7-1.cdninstagram.com/v/t51.82787-15/750554438_18613888075039770_6967571392718459592_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=101&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=nS88YcJ-0jgQ7kNvwHxohFM&_nc_oc=AdpYfHVRvATMZ3sJk30vaZsna-ynXxTypSj5Jpu-rfkzNwonOtrq8RZ0Xk65-g-zyEDBtj1MogOjhziJ6GSPMGUv&_nc_zt=23&_nc_ht=scontent-lax7-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=sQ6trQafXGDFU8qaY1a-Lg&_nc_tpa=Q5bMBQEKuDLJBiP-iI4H_Bpb1UnVbFpInyA7umHBy8v4wcUxKVslrticm8ckX2cn8hrO8-Scc0eOs-Qf&oh=00_AQBcphncZ_ieVltwLMhquQrHMMAYet-2LmyINcYqBDjM-Q&oe=6A602377",
+        "permalink": "https://www.instagram.com/reel/Da5f_KiiCqO/",
+        "timestamp": "2026-07-17T15:07:35+0000",
+        "likes": 57,
+        "comments": 3
+      },
+      {
+        "caption": "We've been in the business of home since 1906. That's 120 years of front doors, floor plans, and families finding the place they belong. To celebrate our 120th anniversary, we’re taking a look back at",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/746131412_18613361341039770_6897810362588387519_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=106&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=6zOZT_SmuFQQ7kNvwFXD72-&_nc_oc=Ado-DOR0GE5RMLgxca3y_5FzZuX6wGRCckdysGQrEWe67T2HFqPpXpt1VfJwLp_SpdeWnV4jln1zikFEJymL9vI2&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=sQ6trQafXGDFU8qaY1a-Lg&_nc_tpa=Q5bMBQFQghZ6d6WUGUdKhtOy74BcHJTObfcdg6dgzFjgWLqP6EiqUV2lJ42GZU8pIJvz00RBpqpDTvZ9&oh=00_AQCUCnOs97H7YSWp72M4oyVnfxonO2ODI7SZLqiP27rO1w&oe=6A6033F4",
+        "permalink": "https://www.instagram.com/reel/Da0Ii63jDyO/",
+        "timestamp": "2026-07-15T13:06:33+0000",
+        "likes": 210,
+        "comments": 7
+      },
+      {
+        "caption": "Comfy couches.\nThen, now, and forever.",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/746385753_18612654502039770_6513334560268129460_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=104&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=vo0tjlpyUdoQ7kNvwFiSQ_W&_nc_oc=AdpEh_eeVznQtHibOwvw8uNe91hq0Zg7NfMYK_e0vKC-iPUdonjLj1dEQrd3DdCLCu_GiAAcJatT8ST-ZABCsVLI&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=sQ6trQafXGDFU8qaY1a-Lg&oh=00_AQBOGATKNeg0vRnJJdtmZCiWivB_cT3bqmIRBvhyY2Y_jQ&oe=6A60213F",
+        "permalink": "https://www.instagram.com/p/DavAG7GlMNk/",
+        "timestamp": "2026-07-13T13:16:16+0000",
+        "likes": 140,
+        "comments": 3
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:01.686Z",
+    "error": null
+  },
+  "kellerwilliams": {
+    "followers_count": 90905,
+    "posts": [
+      {
+        "caption": "📣 Live Grateful Grass release coming 7/21!\n\n🎻 Recorded at bluegrasshall in Owensboro, Kentucky \n\n🔊 Pre-save now\n\n📍Catch them live\n \n7/24 tapshackduck NC\n7/30 anchorrockclub NJ\n7/31 thehamiltonlive",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.71878-15/730210510_1579190403081188_1913101322738021329_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=103&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=mQEVhERiSz8Q7kNvwHUC3bV&_nc_oc=AdrWPTB_sVEDk4DfoWT_kIxpZLXbzfR5NcZUEsIXv7paJzuVKN_ecvw_LlL2gW6JWIQydMYrtzC8TOhl0V4oSAU6&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=ek8aNlYqXlxzicO5voolbQ&_nc_tpa=Q5bMBQG9-ONgVyE0HiXuAHJp15cownGeSfQ8Mo7pZ_hulMHSGQNJdS29Uqo22dzT8SEum_iNn89NkVEa&oh=00_AQCEtawlr28BZKETCFaWqWfh9ia8zPh91sM_J6Q6Zm7-fg&oe=6A604937",
+        "permalink": "https://www.instagram.com/reel/Da0iLLZsbbc/",
+        "timestamp": "2026-07-15T17:14:37+0000",
+        "likes": 99,
+        "comments": 4
+      },
+      {
+        "caption": "July 17th 🐟 Raleigh lincoln919",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/747177909_18601646926050521_3217427174456035301_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=107&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=FhFI13LuhgkQ7kNvwEb6ilI&_nc_oc=Adqc_TpTSbk0_hudE7XGmUwZqrZ92HtZ401iGIdFDmg9bLAb9WAfuHWAuSvUbnep-HYb-Ime6Eu3gBTND7KXORur&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=ek8aNlYqXlxzicO5voolbQ&_nc_tpa=Q5bMBQHWN5x_6hrXPAULxkrU4T8FKmnWjuZ2UynmaJC7AHddvjff4y_Kkc4uVTzImW33l-FIZBjV_Tpv&oh=00_AQDZkLTUodxhplsqi3gtoGLHq2MAZ_LRxG1Pwf87j_YtUA&oe=6A602634",
+        "permalink": "https://www.instagram.com/reel/Dax8SF9xNr7/",
+        "timestamp": "2026-07-14T16:45:07+0000",
+        "likes": 185,
+        "comments": 14
+      },
+      {
+        "caption": "📣 Raleigh & Charleston THIS weekend! \n\nlincoln919 \nDoors at 10:30 pm \n\nholycitybrewing \nFREE Show!",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/743805665_18601371769050521_5437949942100872796_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=102&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=cz8ESNn0Jo0Q7kNvwEiBBfX&_nc_oc=AdptbOGGZVKlNy3-8UKZ8arnPrKca_WrGuT0o03ygLPt-c8rsjJuIhV-FYyp7qbok7hqQaeJ44S0a6oayJLf4SNb&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=ek8aNlYqXlxzicO5voolbQ&oh=00_AQDqqJJWFxC4UhXVCqpd4C_u8E5PjQjKqbVypYBpfPrb5A&oe=6A602895",
+        "permalink": "https://www.instagram.com/p/DavW9T2FPxC/",
+        "timestamp": "2026-07-13T16:35:51+0000",
+        "likes": 70,
+        "comments": 3
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:02.682Z",
+    "error": null
+  },
+  "theagencyre": {
+    "followers_count": 511210,
+    "posts": [
+      {
+        "caption": "Set behind a private gate in Encino, 4938 Hesperia Ave is a brand-new 2026 construction featuring 5 bedrooms, 5.5 baths, and a media room across 4,300+ sq ft on a 7,000+ sq ft lot.\n\nA striking natural",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/748939043_18604725730040393_8245134735345340467_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=111&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=gko_PEmYeG8Q7kNvwEy0Q4H&_nc_oc=AdobgjjFwQJMndTU5RbbAzX2BLDlI8knGYlRpT9VyFJq5GlNfYhN9fPptI7Rlbd1x7HFIzQsefGIPLkA7Saxwygb&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=l8XCPZGHKKaWjQ-jRnTJ7A&oh=00_AQD4Qggu7v966pUcvfXoqZLFZ2O9Ddb5PntKEhpaj-mzFw&oe=6A6026AD",
+        "permalink": "https://www.instagram.com/p/Da5hpXcFBAE/",
+        "timestamp": "2026-07-17T15:21:39+0000",
+        "likes": 61,
+        "comments": 6
+      },
+      {
+        "caption": "Mar a Mar is not simply an oceanfront residence. It is a singular intersection of heritage, geography, privacy, and lifestyle, set inside Casco Viejo, Panama’s UNESCO World Heritage district, with the",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/746947056_18604476892040393_1000854091231954752_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=103&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=J4DH3hgO58QQ7kNvwHFuky2&_nc_oc=AdptaV1L-1y2qgiyG0ukM6vMDmlm_ZNDFunx7nl4Rp6emJQXkULuVYh1Gidw7Gh0IBA7j0AL56D84U5JLcsuKgms&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=l8XCPZGHKKaWjQ-jRnTJ7A&oh=00_AQD-Gu-Iua7Gvv2pB7eCtnchAf2geUfd33c-R89be0_jGQ&oe=6A601BDD",
+        "permalink": "https://www.instagram.com/p/Da3P_wSGvyI/",
+        "timestamp": "2026-07-16T18:08:57+0000",
+        "likes": 514,
+        "comments": 2
+      },
+      {
+        "caption": "Discover modern Mediterranean luxury in Calabasas’ prestigious Mountain View Estates. This fully remodeled six-bedroom estate features designer finishes, seamless indoor-outdoor living, a gourmet kitc",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/748541082_18604456504040393_6197290889385247799_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=104&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=1ugvfxMoF8MQ7kNvwGuwQSb&_nc_oc=AdohnnPWqEyY0pnohpwZhFkfFkzZ6oxYydxnbtErsjc35vgLyHqljoaMdsPxvoEG4jnE2zRWUtA2rJy9k2F2fOpj&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=l8XCPZGHKKaWjQ-jRnTJ7A&oh=00_AQCG79E3Ko1TJPY9KWKuU6pxGv5u3u0u7oQJmkdJRmQfRw&oe=6A602678",
+        "permalink": "https://www.instagram.com/p/Da3IAPClBuw/",
+        "timestamp": "2026-07-16T16:59:06+0000",
+        "likes": 497,
+        "comments": 10
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:03.660Z",
+    "error": null
+  },
+  "sothebysrealty": {
+    "followers_count": 570046,
+    "posts": [
+      {
+        "caption": "This distinguished home features cedar shingles, stone accents, and heavy timber framing with a rare woodland setting, immersed in a preserved forest canopy.\n\nExtraordinary property of the day represe",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/749066891_1039783681784559_2743534769634914554_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=102&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=0bXILFpeR5gQ7kNvwESLSnF&_nc_oc=Ado1taj_UXBEB4uRFCuuaZHXlex1ZnlrptDW3j-JixFWVaDK70kX60PxJNYtPJmgas2hq_iGEqvECibSQQOaeF0m&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=fmHHwkspLRH-Ns36jSkbuQ&_nc_tpa=Q5bMBQGx-Mjwlm7LNv-1fJMv0u4V6UUAsFAeB_wvhjnGNpN6BafBQFM2-JiDhqESrz8v_Rg1KrRj0-WT&oh=00_AQB_cKa852dDmCbiFjuvvZTNdJVFs2mDDkoic6_blAOTww&oe=6A6040F3",
+        "permalink": "https://www.instagram.com/reel/Da5mvODvYtx/",
+        "timestamp": "2026-07-17T16:07:24+0000",
+        "likes": 63,
+        "comments": 0
+      },
+      {
+        "caption": "The world's leading cities are proving their staying power. \n \nExplore the resilience shaping today's urban markets, and what it means for the future of luxury real estate in our Mid-Year 2026 Luxury ",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/749981378_18609720886032383_1689289886547334587_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=104&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=FqP0Vrz8BE4Q7kNvwFQ2wNH&_nc_oc=Ado61S0z_oVbsv9KGRsaqMbKPLPMiE0vnW2bmGur9CTVuM3WzjemtJXMOL2T3j0fmahAJigiPUPmR3S-4Ca3MGik&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=fmHHwkspLRH-Ns36jSkbuQ&_nc_tpa=Q5bMBQFqXiIM01e1jt5A2b2e5wEN46jK5hS8e8CcF_nucXUtLm3L6fb3rdTUY5tqKgJt5Apy9dr1z9g7&oh=00_AQBNJXGlCRYqKTHVmntCuZbnLp4jHId-qdLawJBzvJVcgQ&oe=6A60361B",
+        "permalink": "https://www.instagram.com/reel/Da3hS5hD1Al/",
+        "timestamp": "2026-07-16T20:41:06+0000",
+        "likes": 357,
+        "comments": 14
+      },
+      {
+        "caption": "Unfolding along a coveted stretch of waterfront, this estate features timeless stone-and-timber craftsmanship, with sweeping lake views, an expansive boathouse, and a rare connection to nature definin",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/748829438_18609661501032383_3618950371051205098_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=104&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=iiY4UE9FXxIQ7kNvwF7962a&_nc_oc=AdrbGGdJiHz2Y5ECbHrLe59HzXbRyf-EG2DUhMsE6-10etKlf5GyKTvpDVlwKTiRcNk2gLYeQFDEc2iU8gMP42-C&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=fmHHwkspLRH-Ns36jSkbuQ&_nc_tpa=Q5bMBQFbxHopDl1nXQafNCLgGJGVp6TGBVEK1Mmmu0mhLhmZArxPhIwKAPQmHRXfvXywi1uTDohZs29C&oh=00_AQD7Cxr1kJWUH1u4yE9Xxut0337641TfgTU6TSa_bJsp2Q&oe=6A601755",
+        "permalink": "https://www.instagram.com/reel/Da3EUYhBmrw/",
+        "timestamp": "2026-07-16T16:27:40+0000",
+        "likes": 304,
+        "comments": 5
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:04.671Z",
+    "error": null
+  },
+  "exprealty": {
+    "followers_count": 118096,
+    "posts": [
+      {
+        "caption": "San Diego, the countdown is on. 🌴\n\nFrom July 28–30, the biggest conversations in residential real estate are happening at Inman Connect, and eXp Realty is taking center stage. Our leaders are steppin",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-1.cdninstagram.com/v/t51.82787-15/747862671_18209490199347274_9033106270069655019_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=104&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=eobbCZuLwUwQ7kNvwHlQEfQ&_nc_oc=Adp2_H0kTlUp2Q0RFERZ_x3DVmW3NIx-fl7PPseHFS_wNeaqJGMlvJ9wWK1DLoAgh0uJ11qDc7DkKOETLq37ayCO&_nc_zt=23&_nc_ht=scontent-lax3-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=W2uSz-AlKVJoi_imxGlAzA&oh=00_AQBNZGgkcbmOWW3I2YtIYDAtPv-01DQLvkC99orkloST-Q&oe=6A601DF5",
+        "permalink": "https://www.instagram.com/p/Da3Xo0gERRV/",
+        "timestamp": "2026-07-16T19:15:01+0000",
+        "likes": 108,
+        "comments": 8
+      },
+      {
+        "caption": "cmmtoday, Former NAHREP National President and Freddie Mac Director of Affordable Lending, is officially an agent at eXp!\n\nIf you've ever wondered what someone who's spent decades inside housing leade",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/749231874_18209481214347274_6338969566137629113_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=106&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=Exwvpt_y2_YQ7kNvwGTsmOH&_nc_oc=AdpnpWZi9JaPgVi5PHIoxaVgfKDUSeJ99oKRqxeVAl9sdGlPCqjn4wTzKHa__4ROIB5vSB1XwW7Gpa2ReLJj5qhA&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=W2uSz-AlKVJoi_imxGlAzA&oh=00_AQDU_RWJLYiv4u9WOp358Iwfc7yA9k3bM_cIi7j1UOpbog&oe=6A603DF8",
+        "permalink": "https://www.instagram.com/p/Da3B24bjB7m/",
+        "timestamp": "2026-07-16T16:05:46+0000",
+        "likes": 201,
+        "comments": 72
+      },
+      {
+        "caption": "We're #eXpProud to be recognized as a Best Company to Work For 🏆\n\nThis recognition reflects what our agents, staff, and leadership have built together: a culture that puts people first, empowers inno",
+        "media_type": "IMAGE",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/748777098_18209395837347274_3529007175881598291_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=100&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiRkVFRC5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=-EQUGmQMyX8Q7kNvwHVrV6D&_nc_oc=AdrwcNRiRshPZRzylk9xlB89Yuj8OkQ59ocATtQHDdLyPasM7muVfCa6fGxQvCfjVN1hUMF0yU-jf0MreSgRiveN&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=W2uSz-AlKVJoi_imxGlAzA&oh=00_AQDfzk_kQE4YP_45R3r5tJ5MAg88tjply-0kLCxL1C7yAA&oe=6A603673",
+        "permalink": "https://www.instagram.com/p/Da1FukPGisj/",
+        "timestamp": "2026-07-15T22:00:54+0000",
+        "likes": 224,
+        "comments": 14
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:06.538Z",
+    "error": null
+  },
+  "lyonstahl": {
+    "followers_count": 1978,
+    "posts": [
+      {
+        "caption": "🏡 Featured Listings 🏡\nFrom prime Los Angeles properties to investment-ready multifamily deals, our latest listings have something for every buyer. Explore standout opportunities across Southern Cali",
+        "media_type": "CAROUSEL_ALBUM",
+        "image": "https://scontent-lax7-1.cdninstagram.com/v/t51.82787-15/748597431_18603976531037179_426772753996930367_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=101&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0FST1VTRUxfSVRFTS5iZXN0X2ltYWdlX3VybGdlbi5DMyJ9&_nc_ohc=zXQpZWYAurEQ7kNvwHOrK-E&_nc_oc=AdqdujpYbCee91hGZLWo7z89zSOA4910wLyNaap6exl4_okW8shxDERehO2v6X2q0ChkzMEmBwiG8YU30Dffh-6A&_nc_zt=23&_nc_ht=scontent-lax7-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=kRQ6VMgG9junWSZXrbJosA&oh=00_AQCqkDEWh4P6qTg-fYiISMkJj7duoQn41KYWzkXVDdF-Dw&oe=6A60336B",
+        "permalink": "https://www.instagram.com/p/Da00nAFEuPT/",
+        "timestamp": "2026-07-15T19:31:14+0000",
+        "likes": 12,
+        "comments": 0
+      },
+      {
+        "caption": "We asked for volunteers to film a day in the life at Lyon Stahl and Emma said \"say less\" 🎬\n\nNot only did she take on the challenge — she even edited the entire thing herself. no notes, no complaints,",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.71878-15/742198735_1758366155514586_7175471187051763776_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=100&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=w31nX0-EokYQ7kNvwHAp8ky&_nc_oc=AdrPJ-wmh1NmIslsDTOBEC8e-i2_z8Hxu_aHD7yQMyrEryRGV5XiWFdHRnFofc-08hA5sqQjD4SJcNFANwOrS8rL&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=kRQ6VMgG9junWSZXrbJosA&_nc_tpa=Q5bMBQFVkGwrFvS5ySTrtttYqRJ-fKQ_vlNfXIniF1JX6g9T-HUBxhoa9cLNit3TZRh7r-jk9QSZewYs&oh=00_AQBGp8KrKVSAoWz8wUQODitTscybnooRvIKATEq5k2dFZA&oe=6A6036D7",
+        "permalink": "https://www.instagram.com/reel/DanuzJ2CfGv/",
+        "timestamp": "2026-07-10T17:31:00+0000",
+        "likes": 18,
+        "comments": 0
+      },
+      {
+        "caption": "Another Playbook Session down 🏆📘\n\nThis time we sat down with David Saghian — 5.5 years in and already building the kind of career most agents are chasing. He broke down his actual playbook for our t",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/743564336_18602173147037179_4531918781795178623_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=103&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=fnPiJFAOv4oQ7kNvwFAhXXH&_nc_oc=AdopNKnV_P7K44pSqb7cDi-lpC3e2dDHDHtUZ98Sy0nY6QkhFZuVXcvFvIXErb8LyTev4sH9z-orb4rAGJAEEEfs&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=kRQ6VMgG9junWSZXrbJosA&_nc_tpa=Q5bMBQE-4Er9Cb_Ugbj2HFdzsfA2JvF1-EsxK2TrP84_GwRw2cbZmGJaRkZrX9yCwdKkS0LDOeW3XHZu&oh=00_AQAmZTfdfXYtNW-H4giaJLFQkSYQZw1lAcWy5fVDs-6X5g&oe=6A60207A",
+        "permalink": "https://www.instagram.com/reel/DalMjVyjn-L/",
+        "timestamp": "2026-07-09T17:55:02+0000",
+        "likes": 61,
+        "comments": 0
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:07.629Z",
+    "error": null
+  },
+  "pinnacleestate": {
+    "followers_count": 3060,
+    "posts": [
+      {
+        "caption": "Anything But a Cup - Skills Class Edition",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax7-1.cdninstagram.com/v/t51.71878-15/748418832_1027754180027175_8147141174603792423_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=105&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=SjU1OwP86DIQ7kNvwHWimqX&_nc_oc=AdrQpP2QRhJk-G9yQoBg9Qat6E3F7qWwkoTrteLuBGrmZHVrAalh3hBJEzYN6PMdC_Li7DJwLleGvwsG1u2UTIki&_nc_zt=23&_nc_ht=scontent-lax7-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=WFCEGcn8dJE3XlLcRz5wYw&_nc_tpa=Q5bMBQFNTd_g64fa-AG9j_eLL-wIYtnXav8wqbGGNARVEleXkBjV1S3dSSkTe7c1Xv1Cg6yml5ueSyjy&oh=00_AQAUXXOhTeoG-ohnngc7cqi5ttrKZGaG6H-gsLf1rAqFog&oe=6A601E4F",
+        "permalink": "https://www.instagram.com/reel/Da4BBvqB9L6/",
+        "timestamp": "2026-07-17T01:21:13+0000",
+        "likes": 196,
+        "comments": 31
+      },
+      {
+        "caption": "What agents aren’t told when they start.\n\n#pinnacle #realestatecareer #hardworkpaysoff #openhouses #realestatesuccess",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/744872878_18596312806063255_3995242043526910660_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=111&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=hqPUXmRtLikQ7kNvwHpvIXK&_nc_oc=AdrWlZa4ydh7ShIa_Vn6ltVIgC-WrMjGep4qmsJ5SOFeOECUj5qdpy40AEZ_3jlRihv2jQvEVml4WQT3RaeuhyIH&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=WFCEGcn8dJE3XlLcRz5wYw&_nc_tpa=Q5bMBQHQzGo8XZoKm34Vxg0_7EoBFpLQzycbhcWu5G0Aqn12jbOpxDsbZegQf1qMvf41zbaHt4waN7pl&oh=00_AQDK7t0aidtKP8ZxBxLqO8QMuTSrLC9oPbDp1xfEGyY5yA&oe=6A6033F3",
+        "permalink": "https://www.instagram.com/reel/DavlsSFBmYY/",
+        "timestamp": "2026-07-13T18:45:49+0000",
+        "likes": null,
+        "comments": 0
+      },
+      {
+        "caption": "The market isnt dead, bad habits are.\n\n#pinnacle #consistency #realestatemindset #prospecting #businessgrowth",
+        "media_type": "VIDEO",
+        "image": "https://scontent-lax7-1.cdninstagram.com/v/t51.82787-15/736376014_18594303505063255_2615921732985036062_n.jpg?stp=dst-jpg_e35_tt6&_nc_cat=101&ccb=7-5&_nc_sid=18de74&efg=eyJlZmdfdGFnIjoiQ0xJUFMuYmVzdF9pbWFnZV91cmxnZW4uQzMifQ%3D%3D&_nc_ohc=_mlgye08ESkQ7kNvwGMpF4_&_nc_oc=AdqwlX4Ep8lRLPU60GGXqJtyeOQlcUwJCJcXPlHuaacG55cPc9_QJ36V0PIWmWT_yt0bp-TcBdcR4Su3GIuqHmbN&_nc_zt=23&_nc_ht=scontent-lax7-1.cdninstagram.com&edm=AL-3X8kEAAAA&_nc_gid=WFCEGcn8dJE3XlLcRz5wYw&_nc_tpa=Q5bMBQHriZV4gF2JdHGlqVbqPA0q-ITDE1ptOcV9qckxTMPKtuecxc87kc-loOzHINrsUycxLB490hsj&oh=00_AQAQ7kU2czUPj6c0FrkzAjSV9yKe2Ufx3YuahyA7kFsGKQ&oe=6A6029A0",
+        "permalink": "https://www.instagram.com/reel/DadkK3EhHi8/",
+        "timestamp": "2026-07-06T18:47:59+0000",
+        "likes": null,
+        "comments": 3
+      }
+    ],
+    "fetchedAt": "2026-07-17T16:42:08.564Z",
+    "error": null
+  }
+}
\ No newline at end of file
diff --git a/data/firm-ig-map.json b/data/firm-ig-map.json
new file mode 100644
index 0000000..0513ade
--- /dev/null
+++ b/data/firm-ig-map.json
@@ -0,0 +1,13 @@
+{
+  "_note": "Brokerage firm -> its public Instagram BUSINESS handle. Keyed by handle -> list of normalized firm-name aliases (lowercase, alphanumeric-only). Business Discovery only reads business/creator accounts, so this covers the major firms; long-tail firms with no business IG fall through to '—'. Extend as needed.",
+  "handles": {
+    "compass": ["compass"],
+    "coldwellbanker": ["coldwellbankerrealty", "coldwellbanker"],
+    "kellerwilliams": ["kellerwilliamsrealty", "kellerwilliams"],
+    "theagencyre": ["theagency", "theagencyre"],
+    "sothebysrealty": ["sothebysinternationalrealty", "sothebysrealty", "sothebys"],
+    "exprealty": ["exprealtyofcaliforniainc", "exprealtyofcalifornia", "exprealty"],
+    "lyonstahl": ["lyonstahlinvestmentrealestate", "lyonstahl"],
+    "pinnacleestate": ["pinnacleestatepropertiesinc", "pinnacleestateproperties"]
+  }
+}
diff --git a/public/index.html b/public/index.html
index a13a3be..d35a1f4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -235,6 +235,25 @@
   .composeActs button.gated { border-color:#5a4a1e; color:#e3b341; }
   .composeActs button.gated:hover:not([disabled]) { border-color:#e3b341; }
   .composeActs button[disabled] { opacity:.4; cursor:not-allowed; }
+  /* Tabbed agent window */
+  .composeCard.tabwin { width:min(680px,94vw); }
+  .composeH .winmeta { color:var(--mut); font-size:11px; font-weight:400; margin-left:10px; }
+  .tabbar { display:flex; gap:2px; border-bottom:1px solid var(--line); margin:4px 0 12px; flex-wrap:wrap; }
+  .tabt { background:transparent; border:0; border-bottom:2px solid transparent; color:var(--mut); font-size:12.5px; padding:7px 12px; cursor:pointer; }
+  .tabt:hover { color:var(--ink); }
+  .tabt.active { color:var(--ink); border-bottom-color:var(--blue); font-weight:600; }
+  .tabpane { display:none; }
+  .tabpane.active { display:block; }
+  .tabmut { color:var(--mut); font-size:12px; font-style:italic; padding:8px 2px; }
+  .cm-row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:10px; }
+  .tabpane .ainrow { display:flex; flex-wrap:wrap; gap:14px; margin:8px 0; }
+  .tabpane .ain { color:var(--mut); text-decoration:none; font-size:12px; }
+  .tabpane .ain:hover { color:var(--blue); }
+  .findgrid { display:flex; flex-direction:column; gap:7px; }
+  .findlink { display:inline-block; background:#141a22; border:1px solid #2a3340; border-radius:8px; color:var(--ink); text-decoration:none; font-size:12.5px; padding:8px 11px; cursor:pointer; text-align:left; }
+  .findlink:hover { border-color:var(--blue); color:var(--blue); }
+  .findlink.findloop { padding:8px 11px; }
+  .findlink.findloop a { color:inherit; text-decoration:none; }
   .crmToast { position:fixed; bottom:22px; left:50%; transform:translateX(-50%) translateY(12px); background:#11161d; border:1px solid #2a3340; color:var(--ink); font-size:12.5px; padding:9px 16px; border-radius:20px; opacity:0; transition:opacity .2s,transform .2s; z-index:9500; pointer-events:none; }
   .crmToast.on { opacity:1; transform:translateX(-50%) translateY(0); }
   /* Card-view agent-contact block (every card, every asset type) */
@@ -465,6 +484,14 @@
     .lead .body{display:none;padding:4px 14px 12px;border-top:1px solid var(--line);}
     .lead.open .body{display:block;}
     .lead .acts{display:flex;gap:8px;margin:10px 0 6px;flex-wrap:wrap;}
+    .figwrap{margin:8px 0 4px;display:flex;flex-direction:column;gap:4px;}
+    .fighd{font-size:11px;color:var(--mut);letter-spacing:.02em;margin-bottom:1px;}
+    .figli{display:flex;gap:9px;align-items:center;padding:5px 7px;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.02);text-decoration:none;color:inherit;}
+    .figli:hover{border-color:var(--blue);}
+    .figt{width:38px;height:38px;flex:0 0 38px;border-radius:6px;object-fit:cover;background:var(--line);display:inline-block;}
+    .figb{display:flex;flex-direction:column;min-width:0;gap:1px;}
+    .figc{font-size:12px;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(70vw,560px);}
+    .figm{font-size:10.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
     .mls{width:100%;border-collapse:collapse;font-size:12.5px;}
     .mls th,.mls td{text-align:left;padding:5px 7px;border-top:1px solid var(--line);} .mls th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.4px;border-top:0;}
     .mls td.n{text-align:right;font-variant-numeric:tabular-nums;} .mls tr.mrow{cursor:pointer;} .mls tr.mrow:hover td{background:rgba(210,153,34,.07);}
@@ -547,8 +574,28 @@
           +(sold.length?sold.map(c=>'<div class="phitem"><span>'+esc(c.address)+', '+esc(c.city)+'</span><span class="m">'+money(c.sold_price)+' · '+String(c.sold_date||'').slice(0,10)+'</span></div>').join(''):'<div class="gate">none</div>');
       }catch(e){ box.innerHTML='<div class="gate">'+esc(e.message)+'</div>'; }
     }
+    // Firm Instagram (Business Discovery — public firm business accounts only).
+    // Loads the last-3-posts cache + firm->handle map, builds a normalized index.
+    window.FIRM_IG={}; window.FIRM_IG_IDX={};
+    const firmIgReady=Promise.all([
+      fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
+      fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}}))
+    ]).then(([cache,map])=>{ window.FIRM_IG=cache||{}; const idx={};
+      for(const [h,al] of Object.entries((map.handles)||{})) for(const a of al) idx[a]=h; window.FIRM_IG_IDX=idx; }).catch(()=>{});
+    const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]/g,'');
+    function firmHandle(firm){ const n=normFirm(firm); if(!n) return null;
+      if(window.FIRM_IG_IDX[n]) return window.FIRM_IG_IDX[n];
+      for(const [a,h] of Object.entries(window.FIRM_IG_IDX)){ if(a.length>=5&&n.includes(a)) return h; } return null; }
+    function firmIgStrip(firm){ const h=firmHandle(firm); if(!h) return '';
+      const c=window.FIRM_IG[h]; if(!c||!c.posts||!c.posts.length) return '';
+      const items=c.posts.slice(0,3).map(p=>{ const cap=esc((p.caption||'').replace(/\s+/g,' ').slice(0,90))||'(no caption)';
+        const when=p.timestamp?String(p.timestamp).slice(0,10):''; const vid=p.media_type==='VIDEO'?'▶ ':'';
+        const thumb=p.image?'<img class="figt" loading="lazy" src="'+esc(p.image)+'" alt="">':'<span class="figt"></span>';
+        return '<a class="figli" href="'+esc(p.permalink)+'" target="_blank" rel="noopener noreferrer">'+thumb+'<span class="figb"><span class="figc">'+vid+cap+' ↗</span><span class="figm">'+(when?when+' · ':'')+'♥ '+(p.likes||0)+' · 💬 '+(p.comments||0)+'</span></span></a>'; }).join('');
+      return '<div class="figwrap"><div class="fighd">📷 @'+esc(h)+' · firm’s latest 3</div>'+items+'</div>'; }
     // Ranked best leads for Frank — expandable to MLS-style current listings + Sold + per-address history.
-    fetch('/api/leads/frank?limit=40').then(r=>r.json()).then(d=>{
+    fetch('/api/leads/frank?limit=40').then(r=>r.json()).then(async d=>{
+      await firmIgReady;
       const leads=d.leads||[];
       if(!leads.length){ bq('#bkleads').innerHTML='<div class="gate">no leads (broker DB unavailable)</div>'; return; }
       bq('#bkleads').innerHTML=leads.map((l,i)=>{
@@ -562,6 +609,7 @@
           +'<span class="fit">fit '+l.fit_score+'</span><span class="meta">'+l.listings+' listings · '+moneyK(l.total_value)+'</span><span class="car">▸</span></div>'
           +'<div class="body"><div class="acts"><button class="bkbtn" data-act="contact">✉ Contact + pitch</button>'
           +'<button class="bkbtn" data-act="sold">🔴 Sold / area history</button></div>'
+          +firmIgStrip(l.firm)
           +'<table class="mls"><thead><tr><th>Address</th><th>Type</th><th>Price</th><th>Units</th><th>Cap</th><th></th></tr></thead><tbody>'+rows+'</tbody></table>'
           +'<div class="soldbox"></div></div></div>';
       }).join('');
@@ -818,11 +866,9 @@ function renderList(rows){
   const cols=listCols();
   const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' ▲':' ▼'):'';
   const head='<th scope="col" class="stick sortable" data-sortk="addr" title="Sort by address">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th scope="col" class="sortable" data-sortk="${c.k}" title="Sort by ${esc(c.label)}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
-  const colspan=cols.length+1;
-  const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}" title="Click the row to open agent contact + deeper searches">`
-    +`<td class="stick"><span class="exptog" aria-hidden="true">▸</span><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
-    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`
-    +`<tr class="lexp" data-for="${p.id}"><td colspan="${colspan}"><div class="lexpbody" id="exp-${p.id}"></div></td></tr>`).join('');
+  const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}" title="Click to open the agent window (contact · letters · find · comps · history)">`
+    +`<td class="stick"><span class="exptog" aria-hidden="true" title="Open agent window">⧉</span><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <span class="la">${esc(p.address)}</span><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
+    +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
   return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
 }
 // The inline panel that opens under a list row: deep property/agent searches + the agent CRM
@@ -1059,11 +1105,8 @@ function card(p){
     ${vis('upside')&&p.upside_note?`<div class="small">💡 ${esc(p.upside_note)}</div>`:''}
     ${vis('demo')?demoLine(p):''}
     ${vis('rent')?rentLine(p):''}
-    ${vis('links')?`<div class="src"><button class="lcbtn" data-id="${p.id}" title="Pull fresh comps inline (~$0.03)">🔄 Live comps</button> <button class="histbtn" data-id="${p.id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (inline, free)">📜 Property history</button>${p.source?` <button class="copyb" data-copyval="${esc(p.source)}" title="Copy the source listing URL">⧉ listing URL</button>`:''}</div>`:''}
     ${anyAssessorVisible()?`<div class="assessline small" data-addr="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}">📋 assessor…</div>`:''}
     ${cardAgentBlock(p)}
-    <div class="lcout" id="lc-${p.id}"></div>
-    <div class="histout" id="hist-${p.id}"></div>
   </div>`;
 }
 // Per-listing agent-contact block for the CARD view — every card (any asset type) gets the same
@@ -1600,33 +1643,70 @@ async function deleteLetter(id,idx){
 }
 function agentDraft(p,a){ return `Hi ${a.name||'there'},\n\nI'm interested in ${p.address||'your listing'}${p.city?' in '+p.city:''}${p.price?' (listed '+fmt(p.price)+')':''}. Could you share the setup — current rents / in-place NOI, any financing in place, and whether an offer near list would be entertained?\n\nThanks,\nFrank`; }
 // The full per-listing contact modal body (editable info + mark-contacted + compose/save + letter log).
+// The single per-listing "Agent window" — ONE modal with tabs: Agent · Letters · Find · Comps · History.
 function contactModalBody(p){
-  const a=agentInfo(p), id=p.id;
+  const a=agentInfo(p), id=p.id, firm=a.firm;
+  const tel=a.phone?`<a class="ain" href="tel:${esc(a.phone).replace(/[^0-9+]/g,'')}">📞 ${esc(a.phone)}</a>`:'';
+  const eml=a.email?`<a class="ain" href="mailto:${esc(a.email)}">✉ ${esc(a.email)}</a>`:'';
   const letters=a.letters.length?`<div class="crm-letters"><div class="deeplbl">Saved letters (${a.letters.length}) — saved, not sent</div>`
-    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'';
-  return `<div class="composeCard">
-    <div class="composeH">🧑‍💼 Agent window — <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<button class="composeX" title="Close">✕</button></div>
-    <div class="cm-info">
-      <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
-      <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
-      <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
-      <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info</button>
-      <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
-      ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
+    +a.letters.map((l,i)=>`<div class="lett"><div class="letth"><b>${esc(l.subject||'(no subject)')}</b>${l.draft_id?`<span class="achip done" title="Gmail draft created${l.to?' to '+esc(l.to):''} — review & send in Gmail">📤 drafted</span>`:''}<span class="letts">${fmtWhen(l.ts)}</span><button class="letdel" data-letdel="${id}" data-idx="${l.id||i}" title="Delete this saved letter">✕</button></div><div class="lettb">${esc(l.body||'')}</div></div>`).join('')+`</div>`:'<div class="tabmut">No saved letters yet.</div>';
+  const find=[];
+  if(firm&&firm!=='Unknown'){
+    find.push(`<a class="findlink" href="${gsearch('"'+firm+'" commercial real estate Los Angeles')}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/companies/?keywords=${encodeURIComponent(firm)}" target="_blank" rel="noopener noreferrer">🏢 ${esc(firm)} — LinkedIn</a>`);
+    find.push(`<button class="findlink" data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings in the grid">▦ More from ${esc(firm)} (in-app)</button>`);
+  }
+  if(a.name){
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'"'+(firm&&firm!=='Unknown'?' "'+firm+'"':'')+' real estate agent')}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — Google</a>`);
+    find.push(`<a class="findlink" href="https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(a.name+(firm&&firm!=='Unknown'?' '+firm:''))}" target="_blank" rel="noopener noreferrer">🧑‍💼 ${esc(a.name)} — LinkedIn</a>`);
+    find.push(`<a class="findlink" href="${gsearch('"'+a.name+'" DRE license California real estate')}" target="_blank" rel="noopener noreferrer">🪪 ${esc(a.name)} — DRE license</a>`);
+  }
+  if(p.source) find.push(`<a class="findlink" href="${safeUrl(p.source)}" target="_blank" rel="noopener noreferrer">↗ View source listing</a>`);
+  find.push(`<span class="findlink findloop">${loopnetLink(p)}</span>`);
+  const findHtml=find.length?`<div class="findgrid">${find.join('')}</div>`:'<div class="tabmut">Add an agent/firm in the Agent tab to enable lookups.</div>';
+  return `<div class="composeCard tabwin">
+    <div class="composeH">🧑‍💼 <b>${esc(p.address||'listing')}</b>${p.city?', '+esc(p.city):''}<span class="winmeta">${a.name?esc(a.name):'no agent yet'}${a.contacted_at?' · ✓ '+fmtWhen(a.contacted_at):''}</span><button class="composeX" title="Close">✕</button></div>
+    <div class="tabbar">
+      <button class="tabt active" data-tab="agent">Agent</button>
+      <button class="tabt" data-tab="letters">Letters${a.letters.length?' ('+a.letters.length+')':''}</button>
+      <button class="tabt" data-tab="find">Find</button>
+      <button class="tabt" data-tab="comps">Comps</button>
+      <button class="tabt" data-tab="history">History</button>
     </div>
-    <div class="cm-notes"><div class="deeplbl">Notes</div>
-      <textarea class="crmf" data-info="notes" data-id="${id}" rows="3" placeholder="Notes on this agent / listing (saved with Save info)">${esc(a.notes||'')}</textarea>
+    <div class="tabpane active" data-pane="agent">
+      <div class="cm-info">
+        <input class="crmf" data-info="name" data-id="${id}" placeholder="Agent name" value="${esc(a.name||'')}">
+        <input class="crmf" data-info="phone" data-id="${id}" placeholder="Phone" value="${esc(a.phone||'')}">
+        <input class="crmf" data-info="email" data-id="${id}" placeholder="Email" value="${esc(a.email||'')}">
+      </div>
+      ${(tel||eml)?`<div class="ainrow">${tel} ${eml}</div>`:''}
+      <div class="cm-notes"><div class="deeplbl">Notes</div><textarea class="crmf" data-info="notes" data-id="${id}" rows="4" placeholder="Notes on this agent / listing">${esc(a.notes||'')}</textarea></div>
+      <div class="cm-row">
+        <button class="abtn saveinfobtn" data-saveinfo="${id}">💾 Save info + notes</button>
+        <button class="abtn markbtn" data-mark="${id}">${a.contacted_at?'↻ Update contacted date':'✓ Mark contacted'}</button>
+        ${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}
+      </div>
     </div>
-    <div class="composeGate">⚑ <b>Save letter</b> stores it in this listing's log. <b>Send via George</b> creates a Gmail <b>DRAFT</b> to the agent — it is NOT sent; you review &amp; hit send in Gmail.</div>
-    <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
-    <textarea id="composeBody" rows="8">${esc(agentDraft(p,a))}</textarea>
-    <div class="composeActs">
-      <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
-      <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill the Email field first) — not sent, you review &amp; send in Gmail">📤 Send via George</button>
-      <button id="composeCopy">📋 Copy</button>
-      <button class="composeX2">Cancel</button>
+    <div class="tabpane" data-pane="letters">
+      <div class="composeGate">⚑ <b>Save letter</b> logs it. <b>Send via George</b> creates a Gmail <b>DRAFT</b> — NOT sent; you review &amp; hit send in Gmail.</div>
+      <input id="composeSubj" placeholder="Subject" value="${esc('Interest in '+(p.address||'your listing')+(p.city?', '+p.city:''))}">
+      <textarea id="composeBody" rows="7">${esc(agentDraft(p,a))}</textarea>
+      <div class="composeActs">
+        <button id="composeSave" class="primary" data-id="${id}">💾 Save letter</button>
+        <button id="composeSend" class="gated" data-id="${id}" title="Create a Gmail DRAFT to the agent (fill Email in the Agent tab) — not sent">📤 Send via George</button>
+        <button id="composeCopy">📋 Copy</button>
+      </div>
+      ${letters}
+    </div>
+    <div class="tabpane" data-pane="find">${findHtml}</div>
+    <div class="tabpane" data-pane="comps">
+      <button class="lcbtn" data-id="${id}" title="Pull fresh comps via a cloud browser (~$0.03)">🔄 Pull live comps (~$0.03)</button>
+      <div class="lcout" id="lc-${id}"></div>
+    </div>
+    <div class="tabpane" data-pane="history">
+      <button class="histbtn" data-id="${id}" data-address="${(p.address||'').replace(/"/g,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Load property history</button>
+      <div class="histout" id="hist-${id}"></div>
     </div>
-    ${letters}
   </div>`;
 }
 // One modal, opens for EVERY listing from any surface. Its own click delegation (it lives on
@@ -1638,6 +1718,11 @@ function openContact(id){
   m.dataset.id=id; m.innerHTML=contactModalBody(p); m.classList.add('on');
   m.onclick=async(e)=>{
     if(e.target===m || e.target.closest('.composeX,.composeX2')){ closeCompose(); return; }
+    const tb=e.target.closest('.tabt'); if(tb){ activateTab(m,tb.dataset.tab); return; }
+    const cv=e.target.closest('[data-copyval]'); if(cv){ navigator.clipboard.writeText(cv.dataset.copyval).then(()=>toast('📋 copied')).catch(()=>{}); return; }
+    const ff=e.target.closest('[data-firmfilter]'); if(ff){ closeCompose(); F.firms=new Set([ff.dataset.firmfilter]); syncChips(); render(); return; }
+    const lc=e.target.closest('.lcbtn'); if(lc){ fetchLiveComps(lc.dataset.id); return; }
+    const hi=e.target.closest('.histbtn'); if(hi){ fetchHistory(hi); return; }
     if(e.target.closest('#composeCopy')){ navigator.clipboard.writeText(m.querySelector('#composeBody').value); toast('📋 copied'); return; }
     if(e.target.closest('#composeSave')){
       const subject=m.querySelector('#composeSubj').value, body=m.querySelector('#composeBody').value;
@@ -1663,9 +1748,17 @@ function openContact(id){
     const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(id, ld.dataset.idx); return; }
   };
 }
-// Repaint the open modal in place (after mark/letter changes) — preserves it being open.
+// Switch the active tab within the agent window.
+function activateTab(m,tab){
+  m.querySelectorAll('.tabt').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
+  m.querySelectorAll('.tabpane').forEach(pn=>pn.classList.toggle('active',pn.dataset.pane===tab));
+}
+// Repaint the open modal in place (after mark/letter changes) — preserves open state AND active tab.
 function repaintModal(id){ const m=document.getElementById('composeM'); const p=DATA.ranked.find(x=>x.id===id);
-  if(m && m.classList.contains('on') && m.dataset.id===id && p) m.innerHTML=contactModalBody(p); }
+  if(m && m.classList.contains('on') && m.dataset.id===id && p){
+    const cur=(m.querySelector('.tabt.active')||{}).dataset; const tab=cur?cur.tab:'agent';
+    m.innerHTML=contactModalBody(p); activateTab(m,tab||'agent');
+  } }
 function openCompose(id){ openContact(id); }   // back-compat alias
 function closeCompose(){ const m=document.getElementById('composeM'); if(m) m.classList.remove('on'); }
 
@@ -1773,7 +1866,7 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
     const mk=e.target.closest('[data-mark]'); if(mk){ markContacted(mk.dataset.mark); return; }
     const si=e.target.closest('[data-saveinfo]'); if(si){ saveAgentInfo(si.dataset.saveinfo); return; }
     const ld=e.target.closest('[data-letdel]'); if(ld){ deleteLetter(ld.dataset.letdel, ld.dataset.idx); return; }
-    const row=e.target.closest('.listtbl tr.lrow'); if(row && !e.target.closest('a,button,input,textarea,select')){ toggleExpand(row); return; } };
+    const row=e.target.closest('.listtbl tr.lrow'); if(row && !e.target.closest('a,button,input,textarea,select,[data-firmfilter],[data-copyval]')){ openContact(row.dataset.id); return; } };
   $('#shortbtn').onclick=()=>{ F.shortlist=!F.shortlist; render(); };
   $('#cmpbtn').onclick=openCompare;
   $('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
diff --git a/scripts/refresh-firm-ig.js b/scripts/refresh-firm-ig.js
new file mode 100644
index 0000000..0d3f488
--- /dev/null
+++ b/scripts/refresh-firm-ig.js
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+// Populate data/firm-ig-cache.json with each brokerage firm's last 3 Instagram
+// posts via Instagram Business Discovery (the ToS-clean path — reads only public
+// business/creator accounts). Reuses the never-expiring Page token from the
+// MCC .env or the secrets master. $0 — Graph API is free.
+//
+// Usage: node scripts/refresh-firm-ig.js
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const ROOT = path.join(__dirname, '..');
+const GRAPH = 'https://graph.facebook.com/v23.0';
+const MAP = path.join(ROOT, 'data', 'firm-ig-map.json');
+const CACHE = path.join(ROOT, 'data', 'firm-ig-cache.json');
+
+function readEnvVal(file, key) {
+  try { return (fs.readFileSync(file, 'utf8').match(new RegExp('^' + key + '=(.+)$', 'm')) || [])[1]; }
+  catch { return null; }
+}
+function tokenCandidates() {
+  const files = [
+    path.join(os.homedir(), 'Projects/marketing-command-center/.env'),
+    path.join(os.homedir(), 'Projects/secrets-manager/.env'),
+    path.join(ROOT, '.env'),
+  ];
+  const out = [];
+  for (const k of ['IG_ACCESS_TOKEN', 'META_ACCESS_TOKEN']) for (const f of files) {
+    const v = readEnvVal(f, k);
+    if (v && !/TOKEN\s*$/i.test(v) && v.startsWith('EAA') && !out.includes(v)) out.push(v);
+  }
+  return out;
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function resolveIgNode(tok) {
+  let r = await fetch(`${GRAPH}/me?fields=name,instagram_business_account{id,username}&access_token=${encodeURIComponent(tok)}`);
+  let j = await r.json();
+  if (!j.error && j.instagram_business_account) return { id: j.instagram_business_account.id, username: j.instagram_business_account.username };
+  r = await fetch(`${GRAPH}/me/accounts?fields=name,instagram_business_account{id,username}&limit=100&access_token=${encodeURIComponent(tok)}`);
+  j = await r.json();
+  if (j.error) throw new Error(j.error.message);
+  const p = (j.data || []).find(p => p.instagram_business_account);
+  if (!p) throw new Error('no IG business account linked to this token');
+  return { id: p.instagram_business_account.id, username: p.instagram_business_account.username };
+}
+
+async function fetchPosts(igId, tok, username) {
+  const fields = `business_discovery.username(${username}){username,followers_count,media_count,media.limit(3){id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count}}`;
+  const r = await fetch(`${GRAPH}/${igId}?fields=${encodeURIComponent(fields)}&access_token=${encodeURIComponent(tok)}`);
+  const j = await r.json();
+  if (j.error) throw new Error(j.error.message);
+  const bd = j.business_discovery || {};
+  return {
+    followers_count: bd.followers_count ?? null,
+    posts: (bd.media?.data || []).slice(0, 3).map(m => ({
+      caption: (m.caption || '').slice(0, 200), media_type: m.media_type,
+      image: m.media_type === 'VIDEO' ? (m.thumbnail_url || m.media_url) : m.media_url,
+      permalink: m.permalink, timestamp: m.timestamp,
+      likes: m.like_count ?? null, comments: m.comments_count ?? null,
+    })),
+  };
+}
+
+(async () => {
+  const cands = tokenCandidates();
+  if (!cands.length) { console.error('No usable Meta token (IG_ACCESS_TOKEN / META_ACCESS_TOKEN).'); process.exit(1); }
+  const handles = Object.keys(JSON.parse(fs.readFileSync(MAP, 'utf8')).handles || {});
+  let tok, ig;
+  for (const c of cands) { try { ig = await resolveIgNode(c); tok = c; break; } catch (e) { console.log(`  (token …${c.slice(-4)} unusable: ${e.message.slice(0,50)})`); } }
+  if (!tok) { console.error('No candidate token resolved an IG node.'); process.exit(1); }
+  console.log(`Discovery node: @${ig.username}`);
+  const cache = (() => { try { return JSON.parse(fs.readFileSync(CACHE, 'utf8')); } catch { return {}; } })();
+  let ok = 0, failed = 0;
+  for (const h of handles) {
+    try {
+      const data = await fetchPosts(ig.id, tok, h);
+      cache[h] = { ...data, fetchedAt: new Date().toISOString(), error: null };
+      ok++; console.log(`  ✓ @${h} — ${data.posts.length} posts, ${data.followers_count} followers`);
+    } catch (e) {
+      cache[h] = { ...(cache[h] || {}), error: e.message, fetchedAt: new Date().toISOString() };
+      failed++; console.log(`  ✗ @${h} — ${e.message}`);
+    }
+    fs.writeFileSync(CACHE, JSON.stringify(cache, null, 2));
+    await sleep(400);
+  }
+  console.log(`Done: ${ok} ok, ${failed} failed of ${handles.length}. → ${CACHE}`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });

← 4d0bb9f Agent CRM: all-inline (no outside links) + inline Notes fiel  ·  back to Commercialrealestate  ·  CRCP firm-IG: cache thumbnails locally (fix IG hotlink block 4211af8 →