[object Object]

← back to Norma

feat(modals): fan out ResizableModal to 22 modals across 15 files

ee9135a577094d1b2a04a46b051f306c125c7761 · 2026-05-20 13:38:31 -0700 · Steve Abrams

Dispatched 4 parallel claude --print workers via the max-it skill. Each
worker refactored a batch of modal-bearing components to use the shared
<ResizableModal modalId="…"/> wrapper, picking up full-screen toggle,
corner drag-resize, and per-modalId localStorage size persistence.

Modals converted (22):
  ApiCredentialsSection (4)  — api-key-create / api-platform-connect /
                               api-key-revoke-confirm / api-platform-disconnect-confirm
  ManageTabsModal           — manage-tabs
  AddGrantModal             — grant-create
  AddPetitionModal          — petition-create
  ScheduleModal             — pipeline-schedule
  MemberEmailModal          — member-email-generator
  BulkScheduleModal         — social-bulk-schedule
  CompareModal              — email-analyzer-compare-versions
  ContactsTab               — contacts-import-csv
  InTheNewsTab              — intelligence-discover-articles
  JournalistHub (2)         — journalist-article-detail / journalist-add
  OldLeadsTab               — leads-rekindle-letter
  ApiRegistryModal          — api-registry
  LibraryTab                — library-item-create
  AdvocacyTargets           — advocacy-petition-template
  XSessionsTab              — xsessions-new-scan
  StatementsTab             — statements-generate
  PartnersTab               — partners-add
  OnBoardTab (2)            — onboard-trigger-discovery / onboard-nonprofit-signup
  OutreachPlaybookTab (2)   — outreach-add-pipeline-contact / outreach-generate-letter
  SocialTab (2)             — social-add-stream / social-compose-post
  PlatformPreview (2, prev) — platform-preview-save-confirm / platform-preview-publish-confirm
  ComposeModal (prev)       — email-compose

Skipped (intentionally, per ResizableModal rules):
  PetitionDraftModal        — side-drawer, not centered modal
  PipelineTab/PetitionsTab/GrantsTab/NewsTab/EmailSendsTab/EmailAnalyzer/
    ApiRegistryButton        — no inline modals (delegate to external Modal*)
  DashboardTab, PulseFeed, TopicRadar, NetworkGraph3D, EnhancedMindMap,
    PulseGlobe                — no centered modals (inline panels only)

Pre-existing TS errors in lib/auth.ts (SESSION_SECRET narrowing) +
QueryResultRow generics in unrelated files left untouched.

Files touched

Diff

commit ee9135a577094d1b2a04a46b051f306c125c7761
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 20 13:38:31 2026 -0700

    feat(modals): fan out ResizableModal to 22 modals across 15 files
    
    Dispatched 4 parallel claude --print workers via the max-it skill. Each
    worker refactored a batch of modal-bearing components to use the shared
    <ResizableModal modalId="…"/> wrapper, picking up full-screen toggle,
    corner drag-resize, and per-modalId localStorage size persistence.
    
    Modals converted (22):
      ApiCredentialsSection (4)  — api-key-create / api-platform-connect /
                                   api-key-revoke-confirm / api-platform-disconnect-confirm
      ManageTabsModal           — manage-tabs
      AddGrantModal             — grant-create
      AddPetitionModal          — petition-create
      ScheduleModal             — pipeline-schedule
      MemberEmailModal          — member-email-generator
      BulkScheduleModal         — social-bulk-schedule
      CompareModal              — email-analyzer-compare-versions
      ContactsTab               — contacts-import-csv
      InTheNewsTab              — intelligence-discover-articles
      JournalistHub (2)         — journalist-article-detail / journalist-add
      OldLeadsTab               — leads-rekindle-letter
      ApiRegistryModal          — api-registry
      LibraryTab                — library-item-create
      AdvocacyTargets           — advocacy-petition-template
      XSessionsTab              — xsessions-new-scan
      StatementsTab             — statements-generate
      PartnersTab               — partners-add
      OnBoardTab (2)            — onboard-trigger-discovery / onboard-nonprofit-signup
      OutreachPlaybookTab (2)   — outreach-add-pipeline-contact / outreach-generate-letter
      SocialTab (2)             — social-add-stream / social-compose-post
      PlatformPreview (2, prev) — platform-preview-save-confirm / platform-preview-publish-confirm
      ComposeModal (prev)       — email-compose
    
    Skipped (intentionally, per ResizableModal rules):
      PetitionDraftModal        — side-drawer, not centered modal
      PipelineTab/PetitionsTab/GrantsTab/NewsTab/EmailSendsTab/EmailAnalyzer/
        ApiRegistryButton        — no inline modals (delegate to external Modal*)
      DashboardTab, PulseFeed, TopicRadar, NetworkGraph3D, EnhancedMindMap,
        PulseGlobe                — no centered modals (inline panels only)
    
    Pre-existing TS errors in lib/auth.ts (SESSION_SECRET narrowing) +
    QueryResultRow generics in unrelated files left untouched.
---
 components/ManageTabsModal.tsx                 | 370 ++++++++++---------------
 components/api-registry/ApiRegistryModal.tsx   | 206 ++++++--------
 components/contacts/ContactsTab.tsx            |  94 +++----
 components/email-analyzer/CompareModal.tsx     |  69 +----
 components/email-sends/ComposeModal.tsx        | 133 +++------
 components/email-sends/MemberEmailModal.tsx    | 206 +++++---------
 components/geo/AdvocacyTargets.tsx             |  94 +++----
 components/grants/AddGrantModal.tsx            | 167 ++++-------
 components/intelligence/InTheNewsTab.tsx       | 108 ++------
 components/journalists/JournalistHub.tsx       | 298 +++++++++-----------
 components/leads/OldLeadsTab.tsx               | 117 ++++----
 components/library/LibraryTab.tsx              | 227 ++++++++-------
 components/onboard/OnBoardTab.tsx              | 252 +++++++----------
 components/outreach/OutreachPlaybookTab.tsx    | 229 +++++++--------
 components/partners/PartnersTab.tsx            | 116 ++++----
 components/petitions/AddPetitionModal.tsx      | 233 ++++++----------
 components/petitions/PlatformPreview.tsx       | 121 +++-----
 components/pipeline/ScheduleModal.tsx          |  98 +++----
 components/settings/ApiCredentialsSection.tsx  | 337 +++++++++++-----------
 components/social/SocialTab.tsx                | 138 +++------
 components/social/phase2/BulkScheduleModal.tsx | 187 +++++--------
 components/statements/StatementsTab.tsx        |  89 +++---
 components/xsessions/XSessionsTab.tsx          | 156 +++++------
 23 files changed, 1621 insertions(+), 2424 deletions(-)

diff --git a/components/ManageTabsModal.tsx b/components/ManageTabsModal.tsx
index db8047b..8dd4fc1 100644
--- a/components/ManageTabsModal.tsx
+++ b/components/ManageTabsModal.tsx
@@ -1,8 +1,9 @@
 'use client';
 
-import { useState, useMemo, useCallback } from 'react';
-import { X, Eye, EyeOff, Search, RotateCcw } from 'lucide-react';
+import { useState, useMemo } from 'react';
+import { Eye, EyeOff, Search, RotateCcw } from 'lucide-react';
 import type { TabId } from './Sidebar';
+import ResizableModal from './shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -64,248 +65,171 @@ export default function ManageTabsModal({
 
   const hiddenCount = hiddenTabs.size;
 
-  const handleBackdropClick = useCallback(
-    (e: React.MouseEvent) => {
-      if (e.target === e.currentTarget) onClose();
-    },
-    [onClose],
-  );
-
   if (!open) return null;
 
   return (
-    <div
-      onClick={handleBackdropClick}
-      style={{
-        position: 'fixed',
-        inset: 0,
-        zIndex: 200,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        backgroundColor: 'rgba(0, 0, 0, 0.5)',
-        backdropFilter: 'blur(4px)',
-      }}
+    <ResizableModal
+      modalId="manage-tabs"
+      title={
+        <div>
+          <div style={{ fontSize: 16, fontWeight: 700 }}>Manage Tabs</div>
+          <div style={{ fontSize: 11, color: 'var(--color-text-muted)', fontWeight: 400, marginTop: 2 }}>
+            {hiddenCount === 0
+              ? 'All tabs visible'
+              : `${hiddenCount} tab${hiddenCount === 1 ? '' : 's'} hidden`}
+          </div>
+        </div>
+      }
+      onClose={onClose}
+      defaultWidth={520}
     >
+      {/* Search + Reset */}
       <div
-        role="dialog"
-        aria-modal="true"
-        aria-label="Manage sidebar tabs"
         style={{
-          width: '100%',
-          maxWidth: 480,
-          maxHeight: '80vh',
           display: 'flex',
-          flexDirection: 'column',
-          backgroundColor: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
-          overflow: 'hidden',
+          alignItems: 'center',
+          gap: 8,
+          marginBottom: 12,
         }}
       >
-        {/* Header */}
-        <div
-          style={{
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'space-between',
-            padding: '16px 20px 12px',
-            borderBottom: '1px solid var(--color-border)',
-            flexShrink: 0,
-          }}
-        >
-          <div>
-            <h2
-              style={{
-                fontSize: 16,
-                fontWeight: 700,
-                color: 'var(--color-text)',
-                margin: 0,
-                fontFamily: 'var(--font-heading)',
-              }}
-            >
-              Manage Tabs
-            </h2>
-            <p
-              style={{
-                fontSize: 12,
-                color: 'var(--color-text-muted)',
-                margin: '2px 0 0',
-              }}
-            >
-              {hiddenCount === 0
-                ? 'All tabs visible'
-                : `${hiddenCount} tab${hiddenCount === 1 ? '' : 's'} hidden`}
-            </p>
-          </div>
+        <div style={{ position: 'relative', flex: 1 }}>
+          <Search
+            size={14}
+            style={{
+              position: 'absolute',
+              left: 10,
+              top: '50%',
+              transform: 'translateY(-50%)',
+              color: 'var(--color-text-muted)',
+              pointerEvents: 'none',
+            }}
+          />
+          <input
+            type="text"
+            className="input"
+            placeholder="Filter tabs..."
+            value={search}
+            onChange={(e) => setSearch(e.target.value)}
+            style={{
+              paddingLeft: 32,
+              fontSize: 13,
+              height: 34,
+              width: '100%',
+            }}
+          />
+        </div>
+        {hiddenCount > 0 && (
           <button
             type="button"
-            onClick={onClose}
-            aria-label="Close"
+            onClick={onResetAll}
             className="btn btn-ghost btn-sm"
-            style={{ padding: 6 }}
+            style={{
+              fontSize: 12,
+              gap: 4,
+              flexShrink: 0,
+              whiteSpace: 'nowrap',
+            }}
           >
-            <X size={18} />
+            <RotateCcw size={13} />
+            Show All
           </button>
-        </div>
-
-        {/* Search + Reset */}
-        <div
-          style={{
-            display: 'flex',
-            alignItems: 'center',
-            gap: 8,
-            padding: '10px 20px',
-            borderBottom: '1px solid var(--color-border)',
-            flexShrink: 0,
-          }}
-        >
-          <div style={{ position: 'relative', flex: 1 }}>
-            <Search
-              size={14}
-              style={{
-                position: 'absolute',
-                left: 10,
-                top: '50%',
-                transform: 'translateY(-50%)',
-                color: 'var(--color-text-muted)',
-                pointerEvents: 'none',
-              }}
-            />
-            <input
-              type="text"
-              className="input"
-              placeholder="Filter tabs..."
-              value={search}
-              onChange={(e) => setSearch(e.target.value)}
-              style={{
-                paddingLeft: 32,
-                fontSize: 13,
-                height: 34,
-                width: '100%',
-              }}
-            />
-          </div>
-          {hiddenCount > 0 && (
-            <button
-              type="button"
-              onClick={onResetAll}
-              className="btn btn-ghost btn-sm"
-              style={{
-                fontSize: 12,
-                gap: 4,
-                flexShrink: 0,
-                whiteSpace: 'nowrap',
-              }}
-            >
-              <RotateCcw size={13} />
-              Show All
-            </button>
-          )}
-        </div>
-
-        {/* Tab list */}
-        <div
-          style={{
-            flex: 1,
-            overflowY: 'auto',
-            padding: '8px 12px 16px',
-          }}
-        >
-          {Array.from(sections.entries()).map(([sectionId, tabs]) => (
-            <div key={sectionId} style={{ marginBottom: 12 }}>
-              <div
-                style={{
-                  fontSize: 10,
-                  fontWeight: 700,
-                  letterSpacing: '0.08em',
-                  textTransform: 'uppercase',
-                  color: 'var(--color-text-muted)',
-                  padding: '6px 8px 4px',
-                }}
-              >
-                {SECTION_LABELS[sectionId] || sectionId}
-              </div>
-              {tabs.map((tab) => {
-                const isHidden = hiddenTabs.has(tab.id);
-                return (
-                  <button
-                    key={tab.id}
-                    type="button"
-                    onClick={() => onToggleTab(tab.id)}
-                    style={{
-                      display: 'flex',
-                      alignItems: 'center',
-                      gap: 10,
-                      width: '100%',
-                      padding: '8px 10px',
-                      borderRadius: 'var(--radius-sm)',
-                      border: 'none',
-                      cursor: 'pointer',
-                      backgroundColor: 'transparent',
-                      color: isHidden
-                        ? 'var(--color-text-muted)'
-                        : 'var(--color-text)',
-                      fontSize: 13,
-                      fontWeight: 500,
-                      textAlign: 'left',
-                      opacity: isHidden ? 0.5 : 1,
-                      transition: 'background-color 0.15s, opacity 0.15s',
-                    }}
-                    onMouseEnter={(e) => {
-                      e.currentTarget.style.backgroundColor =
-                        'var(--color-surface-el)';
-                    }}
-                    onMouseLeave={(e) => {
-                      e.currentTarget.style.backgroundColor = 'transparent';
-                    }}
-                  >
-                    {isHidden ? (
-                      <EyeOff
-                        size={15}
-                        style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}
-                      />
-                    ) : (
-                      <Eye
-                        size={15}
-                        style={{ color: 'var(--color-primary)', flexShrink: 0 }}
-                      />
-                    )}
-                    <span style={{ flex: 1 }}>{tab.label}</span>
-                    {isHidden && (
-                      <span
-                        style={{
-                          fontSize: 10,
-                          color: 'var(--color-text-muted)',
-                          padding: '1px 6px',
-                          borderRadius: 10,
-                          border: '1px solid var(--color-border)',
-                        }}
-                      >
-                        hidden
-                      </span>
-                    )}
-                  </button>
-                );
-              })}
-            </div>
-          ))}
+        )}
+      </div>
 
-          {sections.size === 0 && (
+      {/* Tab list */}
+      <div>
+        {Array.from(sections.entries()).map(([sectionId, tabs]) => (
+          <div key={sectionId} style={{ marginBottom: 12 }}>
             <div
               style={{
-                textAlign: 'center',
-                padding: '24px 0',
+                fontSize: 10,
+                fontWeight: 700,
+                letterSpacing: '0.08em',
+                textTransform: 'uppercase',
                 color: 'var(--color-text-muted)',
-                fontSize: 13,
+                padding: '6px 8px 4px',
               }}
             >
-              No tabs match your search
+              {SECTION_LABELS[sectionId] || sectionId}
             </div>
-          )}
-        </div>
+            {tabs.map((tab) => {
+              const isHidden = hiddenTabs.has(tab.id);
+              return (
+                <button
+                  key={tab.id}
+                  type="button"
+                  onClick={() => onToggleTab(tab.id)}
+                  style={{
+                    display: 'flex',
+                    alignItems: 'center',
+                    gap: 10,
+                    width: '100%',
+                    padding: '8px 10px',
+                    borderRadius: 'var(--radius-sm)',
+                    border: 'none',
+                    cursor: 'pointer',
+                    backgroundColor: 'transparent',
+                    color: isHidden
+                      ? 'var(--color-text-muted)'
+                      : 'var(--color-text)',
+                    fontSize: 13,
+                    fontWeight: 500,
+                    textAlign: 'left',
+                    opacity: isHidden ? 0.5 : 1,
+                    transition: 'background-color 0.15s, opacity 0.15s',
+                  }}
+                  onMouseEnter={(e) => {
+                    e.currentTarget.style.backgroundColor =
+                      'var(--color-surface-el)';
+                  }}
+                  onMouseLeave={(e) => {
+                    e.currentTarget.style.backgroundColor = 'transparent';
+                  }}
+                >
+                  {isHidden ? (
+                    <EyeOff
+                      size={15}
+                      style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}
+                    />
+                  ) : (
+                    <Eye
+                      size={15}
+                      style={{ color: 'var(--color-primary)', flexShrink: 0 }}
+                    />
+                  )}
+                  <span style={{ flex: 1 }}>{tab.label}</span>
+                  {isHidden && (
+                    <span
+                      style={{
+                        fontSize: 10,
+                        color: 'var(--color-text-muted)',
+                        padding: '1px 6px',
+                        borderRadius: 10,
+                        border: '1px solid var(--color-border)',
+                      }}
+                    >
+                      hidden
+                    </span>
+                  )}
+                </button>
+              );
+            })}
+          </div>
+        ))}
+
+        {sections.size === 0 && (
+          <div
+            style={{
+              textAlign: 'center',
+              padding: '24px 0',
+              color: 'var(--color-text-muted)',
+              fontSize: 13,
+            }}
+          >
+            No tabs match your search
+          </div>
+        )}
       </div>
-    </div>
+    </ResizableModal>
   );
 }
diff --git a/components/api-registry/ApiRegistryModal.tsx b/components/api-registry/ApiRegistryModal.tsx
index 25ce8b1..4f809a8 100644
--- a/components/api-registry/ApiRegistryModal.tsx
+++ b/components/api-registry/ApiRegistryModal.tsx
@@ -6,9 +6,10 @@
 
 import { useEffect, useMemo, useState } from 'react';
 import {
-  X, Search, CheckCircle2, AlertCircle, MinusCircle, Copy, ExternalLink,
+  Search, CheckCircle2, AlertCircle, MinusCircle, Copy, ExternalLink,
   ChevronDown, ChevronRight, Filter, Eye, Lock,
 } from 'lucide-react';
+import ResizableModal from '../shared/ResizableModal';
 
 type Status = 'set' | 'missing' | 'partial';
 
@@ -282,136 +283,107 @@ export default function ApiRegistryModal({ onClose }: { onClose: () => void }) {
   }, [data, filtered]);
 
   return (
-    <div
-      onClick={onClose}
-      style={{
-        position: 'fixed', inset: 0, zIndex: 200,
-        background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-        padding: 20,
-      }}
+    <ResizableModal
+      modalId="api-registry"
+      title={
+        <div>
+          <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>
+            APIs, Tokens & MCP Servers
+          </div>
+          <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2 }}>
+            Comprehensive admin view. Values never leave the server — set/missing only.
+          </div>
+        </div>
+      }
+      onClose={onClose}
+      defaultWidth={920}
     >
-      <div
-        onClick={e => e.stopPropagation()}
-        style={{
-          background: 'var(--color-bg)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          width: '100%', maxWidth: 920, maxHeight: '92vh',
-          display: 'flex', flexDirection: 'column',
-          boxShadow: '0 24px 80px rgba(0,0,0,0.6)',
-        }}
-      >
-        {/* Header */}
+      {/* Summary bar */}
+      {data && (
         <div style={{
-          padding: '16px 20px', borderBottom: '1px solid var(--color-border)',
-          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
+          padding: '10px 0', marginBottom: 12, borderBottom: '1px solid var(--color-border)',
+          display: 'flex', gap: 16, fontSize: 12, color: 'var(--color-text-secondary)',
+          flexWrap: 'wrap',
         }}>
-          <div>
-            <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>
-              APIs, Tokens & MCP Servers
-            </div>
-            <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2 }}>
-              Comprehensive admin view. Values never leave the server — set/missing only.
-            </div>
-          </div>
-          <button
-            onClick={onClose}
-            className="btn btn-ghost btn-sm"
-            style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
-            aria-label="Close"
-          >
-            <X size={14} />
-          </button>
+          <span><strong style={{ color: 'var(--color-text)' }}>{data.summary.total}</strong> total</span>
+          <span style={{ color: '#34d399' }}>● {data.summary.set} configured</span>
+          <span style={{ color: '#fbbf24' }}>● {data.summary.partial} partial</span>
+          <span style={{ color: '#fb7185' }}>● {data.summary.missing} missing</span>
+          {data.summary.requiredMissing > 0 && (
+            <span style={{ color: '#fb7185', fontWeight: 700 }}>
+              ⚠ {data.summary.requiredMissing} REQUIRED missing
+            </span>
+          )}
         </div>
+      )}
 
-        {/* Summary bar */}
-        {data && (
+      {/* Controls */}
+      <div style={{
+        padding: '12px 0', display: 'flex', gap: 10, alignItems: 'center',
+        borderBottom: '1px solid var(--color-border)', flexWrap: 'wrap', marginBottom: 16,
+      }}>
+        <div style={{ position: 'relative', flex: 1, minWidth: 220 }}>
+          <Search size={14} style={{
+            position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
+            color: 'var(--color-text-muted)',
+          }} />
+          <input
+            className="input"
+            placeholder="Search by name, env var, scope, purpose..."
+            value={search}
+            onChange={e => setSearch(e.target.value)}
+            style={{ paddingLeft: 32, fontSize: 13 }}
+          />
+        </div>
+        <select
+          className="input"
+          value={statusFilter}
+          onChange={e => setStatusFilter(e.target.value as typeof statusFilter)}
+          style={{ fontSize: 12, padding: '6px 10px', minWidth: 180 }}
+        >
+          <option value="all">All statuses</option>
+          <option value="set">Configured only</option>
+          <option value="missing">Missing / partial</option>
+          <option value="required-missing">⚠ Required & missing</option>
+        </select>
+      </div>
+
+      {/* Content */}
+      <div>
+        {loading && (
+          <div style={{ textAlign: 'center', color: 'var(--color-text-muted)', padding: 60 }}>
+            Loading registry...
+          </div>
+        )}
+        {err && (
           <div style={{
-            padding: '10px 20px', borderBottom: '1px solid var(--color-border)',
-            display: 'flex', gap: 16, fontSize: 12, color: 'var(--color-text-secondary)',
-            flexWrap: 'wrap', background: 'var(--color-surface)',
+            padding: 16, borderRadius: 'var(--radius-md)',
+            background: 'rgba(244,63,94,0.12)', color: '#fb7185',
           }}>
-            <span><strong style={{ color: 'var(--color-text)' }}>{data.summary.total}</strong> total</span>
-            <span style={{ color: '#34d399' }}>● {data.summary.set} configured</span>
-            <span style={{ color: '#fbbf24' }}>● {data.summary.partial} partial</span>
-            <span style={{ color: '#fb7185' }}>● {data.summary.missing} missing</span>
-            {data.summary.requiredMissing > 0 && (
-              <span style={{ color: '#fb7185', fontWeight: 700 }}>
-                ⚠ {data.summary.requiredMissing} REQUIRED missing
-              </span>
-            )}
+            {err}
           </div>
         )}
-
-        {/* Controls */}
-        <div style={{
-          padding: '12px 20px', display: 'flex', gap: 10, alignItems: 'center',
-          borderBottom: '1px solid var(--color-border)', flexWrap: 'wrap',
-        }}>
-          <div style={{ position: 'relative', flex: 1, minWidth: 220 }}>
-            <Search size={14} style={{
-              position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
-              color: 'var(--color-text-muted)',
-            }} />
-            <input
-              className="input"
-              placeholder="Search by name, env var, scope, purpose..."
-              value={search}
-              onChange={e => setSearch(e.target.value)}
-              style={{ paddingLeft: 32, fontSize: 13 }}
-            />
+        {!loading && !err && data && grouped.size === 0 && (
+          <div style={{ textAlign: 'center', color: 'var(--color-text-muted)', padding: 60 }}>
+            No integrations match.
           </div>
-          <select
-            className="input"
-            value={statusFilter}
-            onChange={e => setStatusFilter(e.target.value as typeof statusFilter)}
-            style={{ fontSize: 12, padding: '6px 10px', minWidth: 180 }}
-          >
-            <option value="all">All statuses</option>
-            <option value="set">Configured only</option>
-            <option value="missing">Missing / partial</option>
-            <option value="required-missing">⚠ Required & missing</option>
-          </select>
-        </div>
-
-        {/* Content */}
-        <div style={{ flex: 1, overflowY: 'auto', padding: 20 }}>
-          {loading && (
-            <div style={{ textAlign: 'center', color: 'var(--color-text-muted)', padding: 60 }}>
-              Loading registry...
-            </div>
-          )}
-          {err && (
+        )}
+        {Array.from(grouped.entries()).map(([cat, list]) => (
+          <div key={cat} style={{ marginBottom: 24 }}>
             <div style={{
-              padding: 16, borderRadius: 'var(--radius-md)',
-              background: 'rgba(244,63,94,0.12)', color: '#fb7185',
+              fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
+              letterSpacing: '0.12em', color: 'var(--color-text-muted)',
+              marginBottom: 10, paddingBottom: 6,
+              borderBottom: '1px solid var(--color-border)',
             }}>
-              {err}
+              {cat} <span style={{ color: 'var(--color-text-muted)', fontWeight: 400 }}>({list.length})</span>
             </div>
-          )}
-          {!loading && !err && data && grouped.size === 0 && (
-            <div style={{ textAlign: 'center', color: 'var(--color-text-muted)', padding: 60 }}>
-              No integrations match.
+            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
+              {list.map(e => <EntryCard key={e.id} entry={e} />)}
             </div>
-          )}
-          {Array.from(grouped.entries()).map(([cat, list]) => (
-            <div key={cat} style={{ marginBottom: 24 }}>
-              <div style={{
-                fontSize: 11, fontWeight: 700, textTransform: 'uppercase',
-                letterSpacing: '0.12em', color: 'var(--color-text-muted)',
-                marginBottom: 10, paddingBottom: 6,
-                borderBottom: '1px solid var(--color-border)',
-              }}>
-                {cat} <span style={{ color: 'var(--color-text-muted)', fontWeight: 400 }}>({list.length})</span>
-              </div>
-              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
-                {list.map(e => <EntryCard key={e.id} entry={e} />)}
-              </div>
-            </div>
-          ))}
-        </div>
+          </div>
+        ))}
       </div>
-    </div>
+    </ResizableModal>
   );
 }
diff --git a/components/contacts/ContactsTab.tsx b/components/contacts/ContactsTab.tsx
index 05bd915..595a135 100644
--- a/components/contacts/ContactsTab.tsx
+++ b/components/contacts/ContactsTab.tsx
@@ -9,6 +9,7 @@ import {
   ChevronDown, ChevronUp, Link2, ArrowRight,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -782,61 +783,46 @@ export default function ContactsTab() {
       </AnimatePresence>
 
       {/* ── Import Modal ───────────────────────────────────────────────── */}
-      <AnimatePresence>
-        {showImport && (
-          <motion.div
-            initial={{ opacity: 0 }}
-            animate={{ opacity: 1 }}
-            exit={{ opacity: 0 }}
-            style={{ position: 'fixed', inset: 0, zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
+      {showImport && (
+        <ResizableModal
+          modalId="contacts-import-csv"
+          title="Import LinkedIn CSV"
+          onClose={() => setShowImport(false)}
+          defaultWidth={520}
+        >
+          <div
+            onClick={() => fileRef.current?.click()}
+            style={{
+              border: '2px dashed var(--color-border)',
+              borderRadius: 12, padding: 32, textAlign: 'center', cursor: 'pointer',
+              transition: 'border-color 0.15s',
+            }}
+            onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; }}
+            onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; }}
           >
-            <div style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)' }} onClick={() => setShowImport(false)} />
-            <motion.div
-              initial={{ scale: 0.95, opacity: 0 }}
-              animate={{ scale: 1, opacity: 1 }}
-              exit={{ scale: 0.95, opacity: 0 }}
-              className="card"
-              style={{ position: 'relative', padding: 24, width: 440, zIndex: 1 }}
-            >
-              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-                <h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)' }}>Import LinkedIn CSV</h3>
-                <button className="btn btn-ghost btn-sm" onClick={() => setShowImport(false)}><X size={16} /></button>
-              </div>
-              <div
-                onClick={() => fileRef.current?.click()}
-                style={{
-                  border: '2px dashed var(--color-border)',
-                  borderRadius: 12, padding: 32, textAlign: 'center', cursor: 'pointer',
-                  transition: 'border-color 0.15s',
-                }}
-                onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; }}
-                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; }}
-              >
-                <Upload size={32} style={{ margin: '0 auto 8px', color: 'var(--color-text-muted)' }} />
-                <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--color-text)' }}>Click to upload CSV</div>
-                <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 4 }}>
-                  LinkedIn Connections.csv (First Name, Last Name, Email, Company, Position)
-                </div>
-              </div>
-              <input
-                ref={fileRef}
-                type="file"
-                accept=".csv"
-                style={{ display: 'none' }}
-                onChange={e => {
-                  const file = e.target.files?.[0];
-                  if (file) handleImport(file);
-                }}
-              />
-              {importing && (
-                <div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'center', color: 'var(--color-primary)' }}>
-                  <Loader2 size={16} className="spinner" /> Importing...
-                </div>
-              )}
-            </motion.div>
-          </motion.div>
-        )}
-      </AnimatePresence>
+            <Upload size={32} style={{ margin: '0 auto 8px', color: 'var(--color-text-muted)' }} />
+            <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--color-text)' }}>Click to upload CSV</div>
+            <div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 4 }}>
+              LinkedIn Connections.csv (First Name, Last Name, Email, Company, Position)
+            </div>
+          </div>
+          <input
+            ref={fileRef}
+            type="file"
+            accept=".csv"
+            style={{ display: 'none' }}
+            onChange={e => {
+              const file = e.target.files?.[0];
+              if (file) handleImport(file);
+            }}
+          />
+          {importing && (
+            <div style={{ marginTop: 16, display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'center', color: 'var(--color-primary)' }}>
+              <Loader2 size={16} className="spinner" /> Importing...
+            </div>
+          )}
+        </ResizableModal>
+      )}
     </div>
   );
 }
diff --git a/components/email-analyzer/CompareModal.tsx b/components/email-analyzer/CompareModal.tsx
index 8bba0e5..b81bbbb 100644
--- a/components/email-analyzer/CompareModal.tsx
+++ b/components/email-analyzer/CompareModal.tsx
@@ -1,10 +1,10 @@
 'use client';
 
-import { X } from 'lucide-react';
 import type { VersionEntry } from './VersionTimeline';
 import ProsConsPanel, { type ProsConsData } from './ProsConsPanel';
 import { sanitizeHtml } from './sanitize';
 import ScorePanel from './ScorePanel';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -29,66 +29,13 @@ export default function CompareModal({
   onApplySuggestion,
 }: CompareModalProps) {
   return (
-    <div
-      style={{
-        position: 'fixed',
-        inset: 0,
-        zIndex: 100,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        backgroundColor: 'rgba(0,0,0,0.3)',
-      }}
-      onClick={onClose}
+    <ResizableModal
+      modalId="email-analyzer-compare-versions"
+      title={`Compare: v${compareA.versionNumber} vs v${compareB.versionNumber}`}
+      onClose={onClose}
+      defaultWidth={1000}
     >
-      <div
-        onClick={(e) => e.stopPropagation()}
-        style={{
-          width: '90%',
-          maxWidth: 1000,
-          maxHeight: '85vh',
-          overflowY: 'auto',
-          backgroundColor: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 16,
-          boxShadow: '0 12px 48px rgba(0,0,0,0.12)',
-        }}
-      >
-        {/* Header */}
-        <div
-          style={{
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'space-between',
-            padding: '14px 20px',
-            borderBottom: '1px solid var(--color-border)',
-          }}
-        >
-          <h3
-            style={{
-              fontSize: 15,
-              fontWeight: 700,
-              color: 'var(--color-text)',
-              margin: 0,
-            }}
-          >
-            Compare: v{compareA.versionNumber} vs v{compareB.versionNumber}
-          </h3>
-          <button
-            type="button"
-            onClick={onClose}
-            style={{
-              background: 'none',
-              border: 'none',
-              cursor: 'pointer',
-              color: 'var(--color-text-muted)',
-              padding: 4,
-            }}
-          >
-            <X size={16} />
-          </button>
-        </div>
-
+      <div style={{ margin: '-16px -20px' }}>
         {/* Side-by-side content */}
         <div style={{ display: 'flex', minHeight: 300 }}>
           <div
@@ -193,6 +140,6 @@ export default function CompareModal({
           />
         </div>
       </div>
-    </div>
+    </ResizableModal>
   );
 }
diff --git a/components/email-sends/ComposeModal.tsx b/components/email-sends/ComposeModal.tsx
index 01ab104..f73a170 100644
--- a/components/email-sends/ComposeModal.tsx
+++ b/components/email-sends/ComposeModal.tsx
@@ -1,7 +1,6 @@
 'use client';
 
 import { useState, useRef, useEffect, useCallback } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
 import {
   X,
   Loader2,
@@ -33,6 +32,7 @@ import { buildEml, htmlToText } from '@/lib/export';
 import EmojiPicker from './EmojiPicker';
 import ImageDropZone, { useImageDrop, type InlineImage } from './ImageDropZone';
 import HTMLPreview from './HTMLPreview';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -523,92 +523,54 @@ Return ONLY valid JSON in this exact format:
   const showEditor = previewMode !== 'toggle';
   const showPreview = previewMode !== 'off';
 
+  if (!open) return null;
+
   return (
-    <AnimatePresence>
-      {open && (
-        <motion.div
-          initial={{ opacity: 0 }}
-          animate={{ opacity: 1 }}
-          exit={{ opacity: 0 }}
-          style={{
-            position: 'fixed', inset: 0, zIndex: 100,
-            display: 'flex', alignItems: 'center', justifyContent: 'center',
-            backgroundColor: 'rgba(0,0,0,0.35)',
-          }}
-          onClick={handleClose}
-        >
-          <motion.div
-            initial={{ scale: 0.95, opacity: 0 }}
-            animate={{ scale: 1, opacity: 1 }}
-            exit={{ scale: 0.95, opacity: 0 }}
-            onClick={e => e.stopPropagation()}
-            style={{
-              backgroundColor: 'var(--color-surface)',
-              border: '1px solid var(--color-border)',
-              borderRadius: 16,
-              width: '100%',
-              maxWidth: previewMode === 'split' ? 1200 : 900,
-              maxHeight: '90vh',
-              overflowY: 'auto',
-              boxShadow: '0 12px 48px rgba(0,0,0,0.15)',
-              transition: 'max-width 300ms ease',
-            }}
-          >
-            {/* ── Header ──────────────────────────────────────────────────── */}
-            <div
+    <ResizableModal
+      modalId="email-compose"
+      title={
+        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
+          <Mail size={18} style={{ color: 'var(--color-primary)' }} />
+          <span style={{ fontSize: 15, fontWeight: 700, color: 'var(--color-text)' }}>
+            Compose Email
+          </span>
+          {cached && cached.bodyText && (
+            <span style={{
+              fontSize: 11, color: 'var(--color-primary)', fontWeight: 500,
+              padding: '2px 8px', borderRadius: 9999,
+              backgroundColor: 'var(--color-primary-dim)',
+              border: '1px solid rgba(10,124,89,0.25)',
+            }}>
+              Draft restored
+            </span>
+          )}
+          {(subject || topic || bodyText) && (
+            <button
+              onClick={() => {
+                clearCache();
+                setSubject(''); setTopic(''); setBodyText('');
+                setSelectedType('statements'); setTone('formal'); setParagraphs(3);
+                setAudiences(new Set(['all-supporters'])); setCustomEmail('');
+                setInlineImages([]);
+                addToast('Draft discarded', 'info');
+              }}
               style={{
-                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
-                padding: '16px 24px',
-                borderBottom: '1px solid var(--color-border)',
+                background: 'none', border: '1px solid var(--color-border)',
+                borderRadius: 6, cursor: 'pointer', color: 'var(--color-text-muted)',
+                padding: '4px 10px', fontSize: 11, fontWeight: 500,
+                marginLeft: 8,
               }}
             >
-              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
-                <Mail size={18} style={{ color: 'var(--color-primary)' }} />
-                <h3 style={{ fontSize: 15, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
-                  Compose Email
-                </h3>
-                {cached && cached.bodyText && (
-                  <span style={{
-                    fontSize: 11, color: 'var(--color-primary)', fontWeight: 500,
-                    padding: '2px 8px', borderRadius: 9999,
-                    backgroundColor: 'var(--color-primary-dim)',
-                    border: '1px solid rgba(10,124,89,0.25)',
-                  }}>
-                    Draft restored
-                  </span>
-                )}
-              </div>
-              <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
-                {(subject || topic || bodyText) && (
-                  <button
-                    onClick={() => {
-                      clearCache();
-                      setSubject(''); setTopic(''); setBodyText('');
-                      setSelectedType('statements'); setTone('formal'); setParagraphs(3);
-                      setAudiences(new Set(['all-supporters'])); setCustomEmail('');
-                      setInlineImages([]);
-                      addToast('Draft discarded', 'info');
-                    }}
-                    style={{
-                      background: 'none', border: '1px solid var(--color-border)',
-                      borderRadius: 6, cursor: 'pointer', color: 'var(--color-text-muted)',
-                      padding: '4px 10px', fontSize: 11, fontWeight: 500,
-                    }}
-                  >
-                    Discard
-                  </button>
-                )}
-                <button
-                  onClick={handleClose}
-                  style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-text-muted)', padding: 4 }}
-                >
-                  <X size={16} />
-                </button>
-              </div>
-            </div>
-
-            {/* ── Body: Two columns ───────────────────────────────────────── */}
-            <div style={{ display: 'flex', minHeight: 480 }}>
+              Discard
+            </button>
+          )}
+        </span>
+      }
+      onClose={handleClose}
+      defaultWidth={previewMode === 'split' ? 1200 : 900}
+    >
+      {/* ── Body: Two columns ───────────────────────────────────────── */}
+      <div style={{ display: 'flex', minHeight: 480, margin: '-16px -20px' }}>
 
               {/* LEFT — editor area (60% normally, flexible in split mode) */}
               <div style={{
@@ -1212,9 +1174,6 @@ Return ONLY valid JSON in this exact format:
                 )}
               </div>
             </div>
-          </motion.div>
-        </motion.div>
-      )}
-    </AnimatePresence>
+    </ResizableModal>
   );
 }
diff --git a/components/email-sends/MemberEmailModal.tsx b/components/email-sends/MemberEmailModal.tsx
index c3df925..a6cdc6f 100644
--- a/components/email-sends/MemberEmailModal.tsx
+++ b/components/email-sends/MemberEmailModal.tsx
@@ -1,8 +1,7 @@
 'use client';
 
-import { useState, useCallback } from 'react';
+import { useState } from 'react';
 import {
-  X,
   Loader2,
   Sparkles,
   Send,
@@ -20,6 +19,7 @@ import {
 } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
 import { useOrg } from '@/components/OrgProvider';
+import ResizableModal from '@/components/shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -111,13 +111,6 @@ export default function MemberEmailModal({ open, onClose }: MemberEmailModalProp
   const [editBody, setEditBody] = useState('');
   const [copied, setCopied] = useState(false);
 
-  const handleBackdropClick = useCallback(
-    (e: React.MouseEvent) => {
-      if (e.target === e.currentTarget) onClose();
-    },
-    [onClose],
-  );
-
   /* ── Generate email via API ─────────────────────────────────────────────── */
   async function handleGenerate() {
     if (!topic.trim()) {
@@ -194,114 +187,80 @@ export default function MemberEmailModal({ open, onClose }: MemberEmailModalProp
   const activeBody = editMode ? editBody : generatedBody;
 
   return (
-    <div
-      onClick={handleBackdropClick}
-      style={{
-        position: 'fixed',
-        inset: 0,
-        zIndex: 200,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        backgroundColor: 'rgba(0, 0, 0, 0.5)',
-        backdropFilter: 'blur(4px)',
-      }}
+    <ResizableModal
+      modalId="member-email-generator"
+      title={
+        <span style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
+          <span
+            style={{
+              width: 28,
+              height: 28,
+              borderRadius: 8,
+              background: 'linear-gradient(135deg, #10b981, #059669)',
+              display: 'inline-flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              flexShrink: 0,
+            }}
+          >
+            <Sparkles size={15} style={{ color: '#fff' }} />
+          </span>
+          <span style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.15 }}>
+            <span style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)' }}>
+              AI Member Email Generator
+            </span>
+            <span style={{ fontSize: 11, color: 'var(--color-text-muted)', fontWeight: 400 }}>
+              Generate and send targeted emails to your members
+            </span>
+          </span>
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={hasGenerated ? 960 : 580}
+      footer={
+        hasGenerated ? (
+          <>
+            <button
+              type="button"
+              onClick={handleReset}
+              className="btn btn-ghost btn-sm"
+              style={{ fontSize: 12, color: 'var(--color-text-muted)', marginRight: 'auto' }}
+            >
+              Start Over
+            </button>
+            <button
+              type="button"
+              onClick={handleSend}
+              className="btn btn-primary"
+              style={{ gap: 8, fontSize: 13, fontWeight: 700 }}
+            >
+              <Send size={14} />
+              Send to Members
+            </button>
+          </>
+        ) : undefined
+      }
     >
       <div
-        role="dialog"
-        aria-modal="true"
-        aria-label="AI Member Email Generator"
         style={{
-          width: '100%',
-          maxWidth: hasGenerated ? 960 : 580,
-          maxHeight: '90vh',
           display: 'flex',
-          flexDirection: 'column',
-          backgroundColor: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          boxShadow: '0 20px 60px rgba(0, 0, 0, 0.4)',
-          overflow: 'hidden',
-          transition: 'max-width 0.3s ease',
+          gap: 0,
+          marginLeft: -20,
+          marginRight: -20,
+          marginTop: -16,
+          marginBottom: -16,
         }}
       >
-        {/* ── Header ──────────────────────────────────────────────────────────── */}
-        <div
-          style={{
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'space-between',
-            padding: '16px 20px 12px',
-            borderBottom: '1px solid var(--color-border)',
-            flexShrink: 0,
-          }}
-        >
-          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
-            <div
-              style={{
-                width: 32,
-                height: 32,
-                borderRadius: 8,
-                background: 'linear-gradient(135deg, #10b981, #059669)',
-                display: 'flex',
-                alignItems: 'center',
-                justifyContent: 'center',
-              }}
-            >
-              <Sparkles size={16} style={{ color: '#fff' }} />
-            </div>
-            <div>
-              <h2
-                style={{
-                  fontSize: 16,
-                  fontWeight: 700,
-                  color: 'var(--color-text)',
-                  margin: 0,
-                }}
-              >
-                AI Member Email Generator
-              </h2>
-              <p
-                style={{
-                  fontSize: 11,
-                  color: 'var(--color-text-muted)',
-                  margin: '1px 0 0',
-                }}
-              >
-                Generate and send targeted emails to your members
-              </p>
-            </div>
-          </div>
-          <button
-            type="button"
-            onClick={onClose}
-            aria-label="Close"
-            className="btn btn-ghost btn-sm"
-            style={{ padding: 6 }}
-          >
-            <X size={18} />
-          </button>
-        </div>
-
-        {/* ── Content area ────────────────────────────────────────────────────── */}
+        {/* ── Left: Form ────────────────────────────────────────────────────── */}
         <div
           style={{
-            flex: 1,
+            flex: hasGenerated ? '0 0 380px' : '1',
+            padding: '16px 20px 20px',
             overflowY: 'auto',
-            display: 'flex',
-            gap: 0,
+            borderRight: hasGenerated ? '1px solid var(--color-border)' : 'none',
+            transition: 'flex 0.3s ease',
           }}
         >
-          {/* ── Left: Form ────────────────────────────────────────────────────── */}
-          <div
-            style={{
-              flex: hasGenerated ? '0 0 380px' : '1',
-              padding: '16px 20px 20px',
-              overflowY: 'auto',
-              borderRight: hasGenerated ? '1px solid var(--color-border)' : 'none',
-              transition: 'flex 0.3s ease',
-            }}
-          >
             {/* Email Type selector */}
             <label
               style={{
@@ -691,39 +650,6 @@ export default function MemberEmailModal({ open, onClose }: MemberEmailModalProp
             </div>
           )}
         </div>
-
-        {/* ── Footer ──────────────────────────────────────────────────────────── */}
-        {hasGenerated && (
-          <div
-            style={{
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'space-between',
-              padding: '12px 20px',
-              borderTop: '1px solid var(--color-border)',
-              flexShrink: 0,
-            }}
-          >
-            <button
-              type="button"
-              onClick={handleReset}
-              className="btn btn-ghost btn-sm"
-              style={{ fontSize: 12, color: 'var(--color-text-muted)' }}
-            >
-              Start Over
-            </button>
-            <button
-              type="button"
-              onClick={handleSend}
-              className="btn btn-primary"
-              style={{ gap: 8, fontSize: 13, fontWeight: 700 }}
-            >
-              <Send size={14} />
-              Send to Members
-            </button>
-          </div>
-        )}
-      </div>
-    </div>
+    </ResizableModal>
   );
 }
diff --git a/components/geo/AdvocacyTargets.tsx b/components/geo/AdvocacyTargets.tsx
index 44b2968..1caba16 100644
--- a/components/geo/AdvocacyTargets.tsx
+++ b/components/geo/AdvocacyTargets.tsx
@@ -1,8 +1,9 @@
 'use client';
 
 import { useState, useEffect, useCallback } from 'react';
-import { Target, ChevronDown, ChevronUp, AlertTriangle, FileText, X } from 'lucide-react';
+import { Target, ChevronDown, ChevronUp, AlertTriangle, FileText } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 interface ContributingFactor {
@@ -91,64 +92,13 @@ We urge immediate action to address these critical issues in our community.
 [Organization]`;
 
   return (
-    <div
-      style={{
-        position: 'fixed',
-        inset: 0,
-        backgroundColor: 'rgba(0,0,0,0.6)',
-        zIndex: 100,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        padding: 20,
-      }}
-      onClick={onClose}
-    >
-      <div
-        style={{
-          backgroundColor: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 16,
-          padding: 24,
-          maxWidth: 540,
-          width: '100%',
-          maxHeight: '80vh',
-          overflow: 'auto',
-        }}
-        onClick={(e) => e.stopPropagation()}
-      >
-        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-          <h3 style={{ fontSize: 15, fontWeight: 700, color: 'var(--color-text)' }}>
-            Petition Template — {target.zip}
-          </h3>
-          <button
-            onClick={onClose}
-            style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-text-muted)', padding: 4 }}
-            aria-label="Close modal"
-          >
-            <X size={18} />
-          </button>
-        </div>
-        <textarea
-          readOnly
-          value={template}
-          aria-label="Generated advocacy template"
-          style={{
-            width: '100%',
-            height: 300,
-            padding: '12px 14px',
-            borderRadius: 8,
-            border: '1px solid var(--color-border)',
-            backgroundColor: 'var(--color-surface-el)',
-            color: 'var(--color-text)',
-            fontSize: 12,
-            lineHeight: 1.7,
-            resize: 'vertical',
-            fontFamily: 'inherit',
-            outline: 'none',
-          }}
-        />
-        <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
+    <ResizableModal
+      modalId="advocacy-petition-template"
+      title={`Petition Template — ${target.zip}`}
+      onClose={onClose}
+      defaultWidth={540}
+      footer={
+        <>
           <button
             onClick={() => { navigator.clipboard.writeText(template); }}
             style={{
@@ -179,9 +129,29 @@ We urge immediate action to address these critical issues in our community.
           >
             Close
           </button>
-        </div>
-      </div>
-    </div>
+        </>
+      }
+    >
+      <textarea
+        readOnly
+        value={template}
+        aria-label="Generated advocacy template"
+        style={{
+          width: '100%',
+          height: 300,
+          padding: '12px 14px',
+          borderRadius: 8,
+          border: '1px solid var(--color-border)',
+          backgroundColor: 'var(--color-surface-el)',
+          color: 'var(--color-text)',
+          fontSize: 12,
+          lineHeight: 1.7,
+          resize: 'vertical',
+          fontFamily: 'inherit',
+          outline: 'none',
+        }}
+      />
+    </ResizableModal>
   );
 }
 
diff --git a/components/grants/AddGrantModal.tsx b/components/grants/AddGrantModal.tsx
index fe9a48d..f4a8f23 100644
--- a/components/grants/AddGrantModal.tsx
+++ b/components/grants/AddGrantModal.tsx
@@ -1,9 +1,9 @@
 'use client';
 
 import { useState, useEffect, useRef } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { X, Plus, Award, Info } from 'lucide-react';
+import { Plus, Award, Info } from 'lucide-react';
 import type { Grant } from './GrantCard';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 interface AddGrantModalProps {
@@ -164,13 +164,7 @@ export default function AddGrantModal({
     if (isOpen) setTimeout(() => firstInputRef.current?.focus(), 80);
   }, [isOpen]);
 
-  /* Escape closes */
-  useEffect(() => {
-    if (!isOpen) return;
-    const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
-    document.addEventListener('keydown', handler);
-    return () => document.removeEventListener('keydown', handler);
-  }, [isOpen, onClose]);
+  /* Escape closes — handled by ResizableModal */
 
   function set(field: keyof FormState, value: string) {
     setForm((prev) => ({ ...prev, [field]: value }));
@@ -265,79 +259,57 @@ export default function AddGrantModal({
   }
 
   /* ─── Render ──────────────────────────────────────────────────────────── */
+  if (!isOpen) return null;
+
   return (
-    <AnimatePresence>
-      {isOpen && (
+    <ResizableModal
+      modalId={isEditMode ? 'grant-edit' : 'grant-create'}
+      title={
+        <span className="flex items-center gap-2">
+          <Award size={14} style={{ color: 'var(--color-primary)' }} aria-hidden="true" />
+          {isEditMode ? 'Edit Grant' : 'Add Grant'}
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={780}
+      footer={
         <>
-          {/* Backdrop */}
-          <motion.div
-            key="backdrop"
-            initial={{ opacity: 0 }}
-            animate={{ opacity: 1 }}
-            exit={{ opacity: 0 }}
-            transition={{ duration: 0.18 }}
+          <button
+            type="button"
             onClick={onClose}
-            style={{
-              position: 'fixed', inset: 0,
-              backgroundColor: 'rgba(0,0,0,0.65)',
-              backdropFilter: 'blur(3px)',
-              zIndex: 40,
-            }}
-            aria-hidden="true"
-          />
-
-          {/* Modal panel */}
-          <motion.div
-            key="modal"
-            initial={{ opacity: 0, scale: 0.96, y: 12 }}
-            animate={{ opacity: 1, scale: 1,    y: 0  }}
-            exit={{   opacity: 0, scale: 0.96, y: 12  }}
-            transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
-            role="dialog"
-            aria-modal="true"
-            aria-label={isEditMode ? 'Edit grant' : 'Add grant'}
-            style={{
-              position: 'fixed', inset: 0,
-              display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
-              zIndex: 50,
-              padding: '1rem',
-              paddingTop: 'clamp(1rem, 5vh, 3rem)',
-              overflowY: 'auto',
-            }}
+            disabled={submitting}
+            className="btn btn-secondary"
+          >
+            Cancel
+          </button>
+          <button
+            type="button"
+            onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
+            disabled={submitting}
+            className="btn btn-primary"
           >
-            <div
-              style={{
-                backgroundColor: 'var(--color-surface)',
-                border: '1px solid var(--color-border)',
-                borderRadius: 'var(--radius-lg)',
-                width: '100%', maxWidth: 780,
-                boxShadow: '0 24px 60px rgba(0,0,0,0.5)',
-                marginBottom: '2rem',
-              }}
-            >
-              {/* Header */}
-              <div
-                className="flex items-center justify-between px-5 py-4"
-                style={{ borderBottom: '1px solid var(--color-border)' }}
-              >
-                <div className="flex items-center gap-2">
-                  <Award size={16} style={{ color: 'var(--color-primary)' }} aria-hidden="true" />
-                  <h2
-                    id="grant-modal-title"
-                    className="font-semibold text-sm"
-                    style={{ color: 'var(--color-text)' }}
-                  >
-                    {isEditMode ? 'Edit Grant' : 'Add Grant'}
-                  </h2>
-                </div>
-                <button type="button" onClick={onClose} className="btn btn-ghost btn-sm" aria-label="Close modal">
-                  <X size={15} aria-hidden="true" />
-                </button>
-              </div>
-
-              {/* Form */}
-              <form onSubmit={handleSubmit} noValidate>
-                <div className="px-5 py-5">
+            {submitting ? (
+              <>
+                <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
+                {isEditMode ? 'Saving...' : 'Adding...'}
+              </>
+            ) : isEditMode ? (
+              'Save Changes'
+            ) : (
+              <>
+                <Plus size={15} aria-hidden="true" />
+                Add Grant
+              </>
+            )}
+          </button>
+        </>
+      }
+    >
+      <form
+        onSubmit={handleSubmit}
+        noValidate
+        aria-labelledby="grant-modal-title"
+      >
                   {/* Usage guidance */}
                   <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '4px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
                     <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
@@ -623,42 +595,7 @@ export default function AddGrantModal({
                       </div>
                     </div>
                   </div>
-                </div>
-
-                {/* Footer */}
-                <div
-                  className="flex items-center justify-end gap-2 px-5 py-4"
-                  style={{ borderTop: '1px solid var(--color-border)' }}
-                >
-                  <button
-                    type="button"
-                    onClick={onClose}
-                    disabled={submitting}
-                    className="btn btn-secondary"
-                  >
-                    Cancel
-                  </button>
-                  <button type="submit" disabled={submitting} className="btn btn-primary">
-                    {submitting ? (
-                      <>
-                        <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
-                        {isEditMode ? 'Saving...' : 'Adding...'}
-                      </>
-                    ) : isEditMode ? (
-                      'Save Changes'
-                    ) : (
-                      <>
-                        <Plus size={15} aria-hidden="true" />
-                        Add Grant
-                      </>
-                    )}
-                  </button>
-                </div>
-              </form>
-            </div>
-          </motion.div>
-        </>
-      )}
-    </AnimatePresence>
+      </form>
+    </ResizableModal>
   );
 }
diff --git a/components/intelligence/InTheNewsTab.tsx b/components/intelligence/InTheNewsTab.tsx
index e90d424..2fbaf0d 100644
--- a/components/intelligence/InTheNewsTab.tsx
+++ b/components/intelligence/InTheNewsTab.tsx
@@ -20,12 +20,13 @@ import { motion, AnimatePresence } from 'framer-motion';
 import {
   Newspaper, Users, TrendingUp,
   ArrowUp, ArrowDown, Mail, FileText, AlertTriangle,
-  Radio, Clock, Search, X, ExternalLink, Tag, BookOpen,
+  Radio, Clock, Search, ExternalLink, Tag, BookOpen,
   CheckCircle, Loader, Link2, ChevronDown, ChevronRight,
   Sparkles,
 } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
 import { getOutletFaviconUrl } from '@/lib/outlet-logo';
+import ResizableModal from '@/components/shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -411,13 +412,6 @@ function DiscoverModal({
     inputRef.current?.focus();
   }, []);
 
-  // Close on Escape
-  useEffect(() => {
-    const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
-    window.addEventListener('keydown', handler);
-    return () => window.removeEventListener('keydown', handler);
-  }, [onClose]);
-
   const handleSearch = useCallback(async () => {
     const name = searchName.trim();
     if (name.length < 2) return;
@@ -484,66 +478,35 @@ function DiscoverModal({
   const relevancePct = (score: number) => Math.round(score * 100);
 
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 200,
-        display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
-        paddingTop: 60, paddingBottom: 20,
-        background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(6px)',
-        overflowY: 'auto',
-      }}
-      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
-      role="dialog"
-      aria-modal="true"
-      aria-label="Discover journalist articles"
-    >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95, y: -20 }}
-        animate={{ opacity: 1, scale: 1, y: 0 }}
-        exit={{ opacity: 0, scale: 0.95, y: -20 }}
-        transition={{ duration: 0.2 }}
-        style={{
-          background: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          width: '100%', maxWidth: 720,
-          margin: '0 16px',
-          overflow: 'hidden',
-        }}
-      >
-        {/* Header */}
-        <div style={{
-          padding: '20px 24px 16px',
-          borderBottom: '1px solid var(--color-border)',
-          display: 'flex', alignItems: 'center', gap: 12,
-        }}>
-          <div style={{
-            width: 38, height: 38, borderRadius: 10,
+    <ResizableModal
+      modalId="intelligence-discover-articles"
+      title={
+        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
+          <span style={{
+            width: 28, height: 28, borderRadius: 8,
             background: 'linear-gradient(135deg, #10b981, #6366f1)',
-            display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
+            display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
           }}>
-            <Sparkles size={18} color="#fff" />
-          </div>
-          <div style={{ flex: 1 }}>
-            <h3 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
-              Discover Journalist Articles
-            </h3>
-            <p style={{ fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2 }}>
-              Search by journalist name to find relevant articles and link them to your feed
-            </p>
-          </div>
-          <button
-            className="btn btn-ghost"
-            onClick={onClose}
-            style={{ padding: 6, flexShrink: 0 }}
-            aria-label="Close discover modal"
-          >
-            <X size={18} />
-          </button>
-        </div>
+            <Sparkles size={14} color="#fff" />
+          </span>
+          Discover Journalist Articles
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={720}
+      footer={
+        <button className="btn btn-ghost btn-sm" onClick={onClose}>
+          Close
+        </button>
+      }
+    >
+      <div>
+        <p style={{ fontSize: 12, color: 'var(--color-text-muted)', margin: '0 0 12px' }}>
+          Search by journalist name to find relevant articles and link them to your feed
+        </p>
 
         {/* Search bar */}
-        <div style={{ padding: '16px 24px', borderBottom: '1px solid var(--color-border)' }}>
+        <div style={{ paddingBottom: 16, borderBottom: '1px solid var(--color-border)', marginBottom: 16 }}>
           <div style={{ display: 'flex', gap: 8 }}>
             <div style={{ position: 'relative', flex: 1 }}>
               <Search
@@ -574,7 +537,7 @@ function DiscoverModal({
         </div>
 
         {/* Results */}
-        <div style={{ maxHeight: '60vh', overflowY: 'auto', padding: '16px 24px' }}>
+        <div>
           {/* Meta info */}
           {result && (
             <div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
@@ -796,19 +759,8 @@ function DiscoverModal({
             </div>
           )}
         </div>
-
-        {/* Footer */}
-        <div style={{
-          padding: '12px 24px',
-          borderTop: '1px solid var(--color-border)',
-          display: 'flex', justifyContent: 'flex-end', gap: 8,
-        }}>
-          <button className="btn btn-ghost btn-sm" onClick={onClose}>
-            Close
-          </button>
-        </div>
-      </motion.div>
-    </div>
+      </div>
+    </ResizableModal>
   );
 }
 
diff --git a/components/journalists/JournalistHub.tsx b/components/journalists/JournalistHub.tsx
index 15dfa1d..e4bcb28 100644
--- a/components/journalists/JournalistHub.tsx
+++ b/components/journalists/JournalistHub.tsx
@@ -4,12 +4,13 @@ import { useState, useEffect, useCallback, useRef } from 'react';
 import { motion, AnimatePresence } from 'framer-motion';
 import {
   Search, Filter, UserPlus, Newspaper, Building2, Tag, Calendar,
-  ArrowLeft, ExternalLink, ChevronDown, ChevronRight, X, Users, BookOpen, TrendingUp,
+  ArrowLeft, ExternalLink, ChevronDown, ChevronRight, Users, BookOpen, TrendingUp,
   Mail, Twitter, RefreshCw, SortAsc, Download, CheckSquare, Square,
   Network, Lightbulb, Contact, Globe, Play, Radio, Rss, Share2, Info,
 } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
 import { getOutletFaviconUrl } from '@/lib/outlet-logo';
+import ResizableModal from '@/components/shared/ResizableModal';
 import JournalistMindMap from './JournalistMindMap';
 import JournalistEntityFlow from './JournalistEntityFlow';
 import ReporterSearch from './ReporterSearch';
@@ -1019,149 +1020,119 @@ function ArticlesPanel({ articles }: { articles: Article[] }) {
 /* ═══════════════════════════════════════════════════════════════════════════ */
 function ArticleDetailModal({ article: a, onClose }: { article: Article; onClose: () => void }) {
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 100,
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-        background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
-      }}
-      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
+    <ResizableModal
+      modalId="journalist-article-detail"
+      title={
+        <span style={{ display: 'inline-block', borderLeft: `3px solid ${SENTIMENT_COLORS[a.sentiment] || '#6366f1'}`, paddingLeft: 10, fontSize: 16, fontWeight: 700 }}>
+          {a.title}
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={640}
     >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95, y: 20 }}
-        animate={{ opacity: 1, scale: 1, y: 0 }}
-        exit={{ opacity: 0, scale: 0.95, y: 20 }}
-        style={{
-          background: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          width: '100%', maxWidth: 640, maxHeight: '85vh',
-          overflow: 'auto', padding: 0,
-        }}
-      >
-        {/* Header with sentiment stripe */}
-        <div style={{
-          borderTop: `3px solid ${SENTIMENT_COLORS[a.sentiment] || '#6366f1'}`,
-          padding: '20px 24px 16px',
-          borderBottom: '1px solid var(--color-border)',
+      {/* Meta */}
+      <div className="flex flex-wrap items-center gap-3 mb-3" style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
+        <span style={{ fontWeight: 600, color: 'var(--color-primary)' }}>
+          {a.outlet && (
+            <img
+              src={getOutletFaviconUrl(a.outlet, 24) || ''}
+              alt=""
+              width={14}
+              height={14}
+              style={{ borderRadius: 3, verticalAlign: 'middle', marginRight: 4, display: 'inline-block' }}
+              onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
+            />
+          )}
+          {a.outlet}
+        </span>
+        <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
+          <Calendar size={12} />
+          {new Date(a.published_date).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
+        </span>
+      </div>
+
+      {/* Stats bar */}
+      <div className="flex flex-wrap items-center gap-4 mb-4" style={{ fontSize: 12, paddingBottom: 12, borderBottom: '1px solid var(--color-border)' }}>
+        {a.word_count > 0 && (
+          <span style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-secondary)' }}>
+            <BookOpen size={13} />
+            {a.word_count.toLocaleString()} words
+            <span style={{ color: 'var(--color-text-muted)' }}>({Math.ceil(a.word_count / 250)} min read)</span>
+          </span>
+        )}
+        <span className="badge" style={{
+          fontSize: 10, padding: '2px 8px',
+          backgroundColor: `${SENTIMENT_COLORS[a.sentiment]}20`,
+          color: SENTIMENT_COLORS[a.sentiment],
+          border: `1px solid ${SENTIMENT_COLORS[a.sentiment]}40`,
         }}>
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
-            <div style={{ flex: 1 }}>
-              <h2 style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.3, margin: 0 }}>
-                {a.title}
-              </h2>
-              <div className="flex flex-wrap items-center gap-3 mt-2" style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
-                <span style={{ fontWeight: 600, color: 'var(--color-primary)' }}>
-                  {a.outlet && (
-                    <img
-                      src={getOutletFaviconUrl(a.outlet, 24) || ''}
-                      alt=""
-                      width={14}
-                      height={14}
-                      style={{ borderRadius: 3, verticalAlign: 'middle', marginRight: 4, display: 'inline-block' }}
-                      onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
-                    />
-                  )}
-                  {a.outlet}
-                </span>
-                <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
-                  <Calendar size={12} />
-                  {new Date(a.published_date).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
-                </span>
-              </div>
-            </div>
-            <button onClick={onClose} className="btn btn-ghost" style={{ padding: 4, flexShrink: 0 }}>
-              <X size={18} />
-            </button>
-          </div>
+          {a.sentiment}
+        </span>
+      </div>
 
-          {/* Stats bar */}
-          <div className="flex flex-wrap items-center gap-4 mt-3" style={{ fontSize: 12 }}>
-            {a.word_count > 0 && (
-              <span style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-secondary)' }}>
-                <BookOpen size={13} />
-                {a.word_count.toLocaleString()} words
-                <span style={{ color: 'var(--color-text-muted)' }}>({Math.ceil(a.word_count / 250)} min read)</span>
-              </span>
-            )}
-            <span className="badge" style={{
-              fontSize: 10, padding: '2px 8px',
-              backgroundColor: `${SENTIMENT_COLORS[a.sentiment]}20`,
-              color: SENTIMENT_COLORS[a.sentiment],
-              border: `1px solid ${SENTIMENT_COLORS[a.sentiment]}40`,
-            }}>
-              {a.sentiment}
-            </span>
+      {/* Summary */}
+      {a.summary && (
+        <div style={{ marginBottom: 20 }}>
+          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
+            Summary
           </div>
+          <p style={{ fontSize: 14, color: 'var(--color-text)', lineHeight: 1.7, margin: 0 }}>
+            {a.summary}
+          </p>
         </div>
+      )}
 
-        {/* Body */}
-        <div style={{ padding: '20px 24px' }}>
-          {/* Summary */}
-          {a.summary && (
-            <div style={{ marginBottom: 20 }}>
-              <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
-                Summary
-              </div>
-              <p style={{ fontSize: 14, color: 'var(--color-text)', lineHeight: 1.7, margin: 0 }}>
-                {a.summary}
-              </p>
-            </div>
-          )}
-
-          {/* Topics */}
-          {a.topics?.length > 0 && (
-            <div style={{ marginBottom: 16 }}>
-              <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
-                Topics
-              </div>
-              <div className="flex flex-wrap gap-2">
-                {a.topics.map((t) => (
-                  <span key={t} className="badge" style={{ fontSize: 11, padding: '3px 10px', backgroundColor: '#a855f720', color: '#a855f7', border: '1px solid #a855f740' }}>{t}</span>
-                ))}
-              </div>
-            </div>
-          )}
+      {/* Topics */}
+      {a.topics?.length > 0 && (
+        <div style={{ marginBottom: 16 }}>
+          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
+            Topics
+          </div>
+          <div className="flex flex-wrap gap-2">
+            {a.topics.map((t) => (
+              <span key={t} className="badge" style={{ fontSize: 11, padding: '3px 10px', backgroundColor: '#a855f720', color: '#a855f7', border: '1px solid #a855f740' }}>{t}</span>
+            ))}
+          </div>
+        </div>
+      )}
 
-          {/* Tags */}
-          {a.tags?.length > 0 && (
-            <div style={{ marginBottom: 20 }}>
-              <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
-                Tags
-              </div>
-              <div className="flex flex-wrap gap-2">
-                {a.tags.map((t) => (
-                  <span key={t} className="badge" style={{ fontSize: 11, padding: '3px 10px' }}>{t}</span>
-                ))}
-              </div>
-            </div>
-          )}
+      {/* Tags */}
+      {a.tags?.length > 0 && (
+        <div style={{ marginBottom: 20 }}>
+          <div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted)', marginBottom: 8 }}>
+            Tags
+          </div>
+          <div className="flex flex-wrap gap-2">
+            {a.tags.map((t) => (
+              <span key={t} className="badge" style={{ fontSize: 11, padding: '3px 10px' }}>{t}</span>
+            ))}
+          </div>
+        </div>
+      )}
 
-          {/* Read Article CTA */}
-          {a.url && (
-            <a
-              href={a.url}
-              target="_blank"
-              rel="noopener noreferrer"
-              className="btn btn-primary"
-              style={{
-                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
-                width: '100%', padding: '12px 20px', fontSize: 14, fontWeight: 600,
-                textDecoration: 'none',
-              }}
-            >
-              <ExternalLink size={16} />
-              Read Full Article
-            </a>
-          )}
-          {!a.url && (
-            <div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--color-text-muted)', fontSize: 13 }}>
-              No article URL available
-            </div>
-          )}
+      {/* Read Article CTA */}
+      {a.url && (
+        <a
+          href={a.url}
+          target="_blank"
+          rel="noopener noreferrer"
+          className="btn btn-primary"
+          style={{
+            display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
+            width: '100%', padding: '12px 20px', fontSize: 14, fontWeight: 600,
+            textDecoration: 'none',
+          }}
+        >
+          <ExternalLink size={16} />
+          Read Full Article
+        </a>
+      )}
+      {!a.url && (
+        <div style={{ textAlign: 'center', padding: '12px 0', color: 'var(--color-text-muted)', fontSize: 13 }}>
+          No article URL available
         </div>
-      </motion.div>
-    </div>
+      )}
+    </ResizableModal>
   );
 }
 
@@ -1394,27 +1365,32 @@ function AddJournalistModal({ onClose, onAdded }: { onClose: () => void; onAdded
   const labelStyle = { fontSize: 11, color: 'var(--color-text-muted)', fontWeight: 600, display: 'block', marginBottom: 4 } as React.CSSProperties;
 
   return (
-    <div style={{ position: 'fixed', inset: 0, zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
-      <div style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)' }} onClick={onClose} />
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        className="card"
-        style={{ position: 'relative', zIndex: 1, width: '100%', maxWidth: 520, padding: 24, maxHeight: '90vh', overflowY: 'auto' }}
-        role="dialog"
-        aria-modal="true"
-        aria-labelledby="add-j-title"
-      >
-        <div className="flex items-center justify-between mb-4">
-          <h3 id="add-j-title" style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)' }}>Add Journalist</h3>
-          <button className="btn btn-ghost btn-sm" onClick={onClose}><X size={16} /></button>
-        </div>
-        <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '16px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
-          <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
-          <span>Add a journalist to your watchlist. Enter their name, outlet, and beat to track their coverage of your issues.</span>
-        </div>
+    <ResizableModal
+      modalId="journalist-add"
+      title="Add Journalist"
+      onClose={onClose}
+      defaultWidth={520}
+      footer={
+        <>
+          <button type="button" className="btn btn-ghost btn-sm" onClick={onClose}>Cancel</button>
+          <button
+            type="submit"
+            form="add-journalist-form"
+            className="btn btn-primary btn-sm"
+            disabled={saving || !form.name || !form.outlet}
+          >
+            {saving ? <span className="spinner" style={{ width: 14, height: 14 }} /> : <UserPlus size={14} />}
+            <span className="ml-1">Add Journalist</span>
+          </button>
+        </>
+      }
+    >
+      <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '16px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
+        <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
+        <span>Add a journalist to your watchlist. Enter their name, outlet, and beat to track their coverage of your issues.</span>
+      </div>
 
-        <form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
+      <form id="add-journalist-form" onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
           {/* ── Core Info (always visible) ─────────────────────────────────── */}
           <div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--color-text-muted)', marginBottom: -4 }}>
             Core Info
@@ -1521,15 +1497,7 @@ function AddJournalistModal({ onClose, onAdded }: { onClose: () => void; onAdded
             </div>
           )}
 
-          <div className="flex justify-end gap-2 mt-2">
-            <button type="button" className="btn btn-ghost btn-sm" onClick={onClose}>Cancel</button>
-            <button type="submit" className="btn btn-primary btn-sm" disabled={saving || !form.name || !form.outlet}>
-              {saving ? <span className="spinner" style={{ width: 14, height: 14 }} /> : <UserPlus size={14} />}
-              <span className="ml-1">Add Journalist</span>
-            </button>
-          </div>
-        </form>
-      </motion.div>
-    </div>
+      </form>
+    </ResizableModal>
   );
 }
diff --git a/components/leads/OldLeadsTab.tsx b/components/leads/OldLeadsTab.tsx
index 98e8774..8493a4d 100644
--- a/components/leads/OldLeadsTab.tsx
+++ b/components/leads/OldLeadsTab.tsx
@@ -11,6 +11,7 @@ import {
 import { useToast } from '../ToastProvider';
 import { useDebounce } from '@/hooks/useDebounce';
 import { SkeletonList, SkeletonStats } from '../Skeleton';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 interface Lead {
@@ -220,12 +221,6 @@ function LetterModal({ lead, onClose }: { lead: Lead; onClose: () => void }) {
   const [loading, setLoading] = useState(true);
   const [copied, setCopied] = useState(false);
 
-  useEffect(() => {
-    const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
-    window.addEventListener('keydown', handleKey);
-    return () => window.removeEventListener('keydown', handleKey);
-  }, [onClose]);
-
   useEffect(() => {
     async function gen() {
       try {
@@ -256,71 +251,59 @@ function LetterModal({ lead, onClose }: { lead: Lead; onClose: () => void }) {
   }
 
   return (
-    <div style={{
-      position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.6)',
-      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
-    }} onClick={onClose}>
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        style={{
-          backgroundColor: 'var(--color-surface)', borderRadius: 16,
-          border: '1px solid var(--color-border)', padding: 24,
-          width: '100%', maxWidth: 600, maxHeight: '80vh', overflowY: 'auto',
-        }}
-        onClick={e => e.stopPropagation()}
-      >
-        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-          <h3 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', margin: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
-            <Flame size={18} style={{ color: '#f97316' }} />
-            Rekindle Letter — {lead.name}
-          </h3>
-          <button onClick={onClose} style={{ border: 'none', background: 'transparent', color: 'var(--color-text-muted)', cursor: 'pointer', fontSize: 18 }}>x</button>
+    <ResizableModal
+      modalId="leads-rekindle-letter"
+      title={
+        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
+          <Flame size={18} style={{ color: '#f97316' }} />
+          Rekindle Letter — {lead.name}
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={600}
+    >
+      {loading ? (
+        <div style={{ textAlign: 'center', padding: 40 }}>
+          <Sparkles size={24} style={{ color: '#f97316', animation: 'spin 2s linear infinite' }} />
+          <p style={{ color: 'var(--color-text-muted)', marginTop: 12, fontSize: 13 }}>AI is crafting your rekindle letter...</p>
         </div>
-
-        {loading ? (
-          <div style={{ textAlign: 'center', padding: 40 }}>
-            <Sparkles size={24} style={{ color: '#f97316', animation: 'spin 2s linear infinite' }} />
-            <p style={{ color: 'var(--color-text-muted)', marginTop: 12, fontSize: 13 }}>AI is crafting your rekindle letter...</p>
-          </div>
-        ) : letter ? (
-          <>
-            <div style={{ marginBottom: 12 }}>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Subject Line</label>
-              <div style={{ padding: '8px 12px', borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
-                {letter.subject}
-              </div>
+      ) : letter ? (
+        <>
+          <div style={{ marginBottom: 12 }}>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Subject Line</label>
+            <div style={{ padding: '8px 12px', borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', fontSize: 14, fontWeight: 600, color: 'var(--color-text)' }}>
+              {letter.subject}
             </div>
-            <div style={{ marginBottom: 12 }}>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Letter Body</label>
-              <div style={{ padding: '12px 14px', borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>
-                {letter.body}
-              </div>
+          </div>
+          <div style={{ marginBottom: 12 }}>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Letter Body</label>
+            <div style={{ padding: '12px 14px', borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>
+              {letter.body}
             </div>
-            {letter.tone_notes && (
-              <p style={{ fontSize: 11, color: 'var(--color-text-muted)', fontStyle: 'italic', margin: '0 0 16px' }}>
-                <AlertCircle size={11} style={{ display: 'inline', marginRight: 4 }} />
-                AI Note: {letter.tone_notes}
-              </p>
+          </div>
+          {letter.tone_notes && (
+            <p style={{ fontSize: 11, color: 'var(--color-text-muted)', fontStyle: 'italic', margin: '0 0 16px' }}>
+              <AlertCircle size={11} style={{ display: 'inline', marginRight: 4 }} />
+              AI Note: {letter.tone_notes}
+            </p>
+          )}
+          <div style={{ display: 'flex', gap: 8 }}>
+            <button onClick={handleCopy}
+              style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: '#10b981', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
+              {copied ? 'Copied!' : 'Copy to Clipboard'}
+            </button>
+            {lead.email && (
+              <a href={'mailto:' + lead.email + '?subject=' + encodeURIComponent(letter.subject) + '&body=' + encodeURIComponent(letter.body)}
+                style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: 'rgba(249,115,22,0.15)', color: '#f97316', fontSize: 13, fontWeight: 600, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
+                <Send size={13} /> Open in Email
+              </a>
             )}
-            <div style={{ display: 'flex', gap: 8 }}>
-              <button onClick={handleCopy}
-                style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: '#10b981', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
-                {copied ? 'Copied!' : 'Copy to Clipboard'}
-              </button>
-              {lead.email && (
-                <a href={'mailto:' + lead.email + '?subject=' + encodeURIComponent(letter.subject) + '&body=' + encodeURIComponent(letter.body)}
-                  style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: 'rgba(249,115,22,0.15)', color: '#f97316', fontSize: 13, fontWeight: 600, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 4 }}>
-                  <Send size={13} /> Open in Email
-                </a>
-              )}
-            </div>
-          </>
-        ) : (
-          <p style={{ color: '#f87171', fontSize: 13 }}>Failed to generate letter. Try again.</p>
-        )}
-      </motion.div>
-    </div>
+          </div>
+        </>
+      ) : (
+        <p style={{ color: '#f87171', fontSize: 13 }}>Failed to generate letter. Try again.</p>
+      )}
+    </ResizableModal>
   );
 }
 
diff --git a/components/library/LibraryTab.tsx b/components/library/LibraryTab.tsx
index db916be..a3ac267 100644
--- a/components/library/LibraryTab.tsx
+++ b/components/library/LibraryTab.tsx
@@ -4,12 +4,13 @@ import { useState, useEffect, useCallback } from 'react';
 import { motion, AnimatePresence } from 'framer-motion';
 import {
   Plus, Search, Star, Copy, FileText, Code, Archive,
-  X, RefreshCw, Sparkles, ChevronDown, ChevronUp,
+  RefreshCw, Sparkles, ChevronDown, ChevronUp,
   Tag, Bookmark, ExternalLink, Trash2, Edit3, Check,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
 import { useDebounce } from '@/hooks/useDebounce';
 import SortDropdown from '../shared/SortDropdown';
+import ResizableModal from '../shared/ResizableModal';
 import { useClientSort, SortConfig } from '@/hooks/useClientSort';
 
 const LIB_SORT_OPTIONS = [
@@ -185,136 +186,128 @@ function LibraryModal({
   }
 
   return (
-    <div
-      className="fixed inset-0 z-50 flex items-center justify-center"
-      style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
-      onClick={onClose}
+    <ResizableModal
+      modalId={isEdit ? 'library-item-edit' : 'library-item-create'}
+      title={isEdit ? 'Edit Library Item' : 'Add to Library'}
+      onClose={onClose}
+      defaultWidth={720}
+      footer={
+        <>
+          <button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
+          <button
+            type="button"
+            className="btn btn-primary"
+            disabled={saving || !form.title.trim()}
+            onClick={() => handleSubmit({ preventDefault() {} } as React.FormEvent)}
+          >
+            {saving ? <><RefreshCw size={14} className="animate-spin" /><span>Saving...</span></> : <>{isEdit ? 'Update' : 'Save'}</>}
+          </button>
+        </>
+      }
     >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        className="w-full max-w-2xl mx-4 rounded-xl p-6 max-h-[85vh] overflow-y-auto"
-        style={{ backgroundColor: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
-        onClick={(e) => e.stopPropagation()}
-      >
-        <div className="flex items-center justify-between mb-5">
-          <h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
-            {isEdit ? 'Edit Library Item' : 'Add to Library'}
-          </h2>
-          <button onClick={onClose} className="btn btn-ghost btn-sm"><X size={16} /></button>
+      <form onSubmit={handleSubmit} className="space-y-4">
+        {/* Title */}
+        <div>
+          <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Title</label>
+          <input
+            className="input w-full"
+            value={form.title}
+            onChange={(e) => updateForm('title', e.target.value)}
+            placeholder="e.g. Community Action Alert Template"
+            autoFocus
+          />
         </div>
 
-        <form onSubmit={handleSubmit} className="space-y-4">
-          {/* Title */}
+        {/* Row: Type + Email Type + Voice Mode */}
+        <div className="grid grid-cols-3 gap-3">
           <div>
-            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Title</label>
-            <input
-              className="input w-full"
-              value={form.title}
-              onChange={(e) => updateForm('title', e.target.value)}
-              placeholder="e.g. Community Action Alert Template"
-              autoFocus
-            />
+            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Type</label>
+            <select className="input w-full" value={form.item_type} onChange={(e) => updateForm('item_type', e.target.value)}>
+              <option value="template">Template</option>
+              <option value="snippet">Snippet</option>
+              <option value="archived_draft">Archived Draft</option>
+            </select>
           </div>
-
-          {/* Row: Type + Email Type + Voice Mode */}
-          <div className="grid grid-cols-3 gap-3">
-            <div>
-              <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Type</label>
-              <select className="input w-full" value={form.item_type} onChange={(e) => updateForm('item_type', e.target.value)}>
-                <option value="template">Template</option>
-                <option value="snippet">Snippet</option>
-                <option value="archived_draft">Archived Draft</option>
-              </select>
-            </div>
-            <div>
-              <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Email Type</label>
-              <select className="input w-full" value={form.email_type} onChange={(e) => updateForm('email_type', e.target.value)}>
-                <option value="action">Action</option>
-                <option value="policy">Policy</option>
-                <option value="resources">Resources</option>
-              </select>
-            </div>
-            <div>
-              <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Voice</label>
-              <select className="input w-full" value={form.voice_mode} onChange={(e) => updateForm('voice_mode', e.target.value)}>
-                <option value="formal">Formal</option>
-                <option value="passionate">Passionate</option>
-                <option value="urgent">Urgent</option>
-                <option value="conversational">Conversational</option>
-              </select>
-            </div>
-          </div>
-
-          {/* Description */}
           <div>
-            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Description</label>
-            <input
-              className="input w-full"
-              value={form.description}
-              onChange={(e) => updateForm('description', e.target.value)}
-              placeholder="Brief description of this item"
-            />
+            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Email Type</label>
+            <select className="input w-full" value={form.email_type} onChange={(e) => updateForm('email_type', e.target.value)}>
+              <option value="action">Action</option>
+              <option value="policy">Policy</option>
+              <option value="resources">Resources</option>
+            </select>
           </div>
-
-          {/* Content */}
           <div>
-            <div className="flex items-center justify-between mb-1">
-              <label className="text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }}>Content</label>
-              <button
-                type="button"
-                className="btn btn-ghost btn-sm"
-                onClick={handleGenerate}
-                disabled={generating}
-              >
-                {generating ? (
-                  <><RefreshCw size={13} className="animate-spin" /><span>Generating...</span></>
-                ) : (
-                  <><Sparkles size={13} /><span>AI Generate</span></>
-                )}
-              </button>
-            </div>
-            <textarea
-              className="input w-full"
-              rows={8}
-              value={form.content_text}
-              onChange={(e) => updateForm('content_text', e.target.value)}
-              placeholder="Content text..."
-            />
+            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Voice</label>
+            <select className="input w-full" value={form.voice_mode} onChange={(e) => updateForm('voice_mode', e.target.value)}>
+              <option value="formal">Formal</option>
+              <option value="passionate">Passionate</option>
+              <option value="urgent">Urgent</option>
+              <option value="conversational">Conversational</option>
+            </select>
           </div>
+        </div>
 
-          {/* Tags */}
-          <div>
-            <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Tags (comma-separated)</label>
-            <input
-              className="input w-full"
-              value={form.tags}
-              onChange={(e) => updateForm('tags', e.target.value)}
-              placeholder="e.g. action, community, policy reform"
-            />
+        {/* Description */}
+        <div>
+          <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Description</label>
+          <input
+            className="input w-full"
+            value={form.description}
+            onChange={(e) => updateForm('description', e.target.value)}
+            placeholder="Brief description of this item"
+          />
+        </div>
+
+        {/* Content */}
+        <div>
+          <div className="flex items-center justify-between mb-1">
+            <label className="text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }}>Content</label>
+            <button
+              type="button"
+              className="btn btn-ghost btn-sm"
+              onClick={handleGenerate}
+              disabled={generating}
+            >
+              {generating ? (
+                <><RefreshCw size={13} className="animate-spin" /><span>Generating...</span></>
+              ) : (
+                <><Sparkles size={13} /><span>AI Generate</span></>
+              )}
+            </button>
           </div>
+          <textarea
+            className="input w-full"
+            rows={8}
+            value={form.content_text}
+            onChange={(e) => updateForm('content_text', e.target.value)}
+            placeholder="Content text..."
+          />
+        </div>
 
-          {/* Pin toggle */}
-          <label className="flex items-center gap-2 cursor-pointer">
-            <input
-              type="checkbox"
-              checked={form.is_pinned}
-              onChange={(e) => updateForm('is_pinned', e.target.checked)}
-            />
-            <span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>Pin to top of library</span>
-          </label>
+        {/* Tags */}
+        <div>
+          <label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-secondary)' }}>Tags (comma-separated)</label>
+          <input
+            className="input w-full"
+            value={form.tags}
+            onChange={(e) => updateForm('tags', e.target.value)}
+            placeholder="e.g. action, community, policy reform"
+          />
+        </div>
 
-          {error && <p className="text-sm" style={{ color: '#f87171' }}>{error}</p>}
+        {/* Pin toggle */}
+        <label className="flex items-center gap-2 cursor-pointer">
+          <input
+            type="checkbox"
+            checked={form.is_pinned}
+            onChange={(e) => updateForm('is_pinned', e.target.checked)}
+          />
+          <span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>Pin to top of library</span>
+        </label>
 
-          <div className="flex justify-end gap-2 pt-2">
-            <button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
-            <button type="submit" className="btn btn-primary" disabled={saving || !form.title.trim()}>
-              {saving ? <><RefreshCw size={14} className="animate-spin" /><span>Saving...</span></> : <>{isEdit ? 'Update' : 'Save'}</>}
-            </button>
-          </div>
-        </form>
-      </motion.div>
-    </div>
+        {error && <p className="text-sm" style={{ color: '#f87171' }}>{error}</p>}
+      </form>
+    </ResizableModal>
   );
 }
 
diff --git a/components/onboard/OnBoardTab.tsx b/components/onboard/OnBoardTab.tsx
index 0f347fe..8ca4c5d 100644
--- a/components/onboard/OnBoardTab.tsx
+++ b/components/onboard/OnBoardTab.tsx
@@ -31,7 +31,6 @@ import {
   XCircle,
   Search,
   Filter,
-  X,
   ClipboardList,
   Globe,
   Users,
@@ -40,6 +39,7 @@ import {
   Vote,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ─────────────────────────────────────────────────────────────────── */
 
@@ -404,105 +404,67 @@ function TriggerDialog({
   }
 
   return (
-    <motion.div
-      initial={{ opacity: 0 }}
-      animate={{ opacity: 1 }}
-      exit={{ opacity: 0 }}
-      style={{
-        position: 'fixed',
-        inset: 0,
-        backgroundColor: 'rgba(0,0,0,0.6)',
-        backdropFilter: 'blur(4px)',
-        zIndex: 50,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        padding: 16,
-      }}
-      onClick={onClose}
-    >
-      <motion.div
-        initial={{ scale: 0.92, opacity: 0 }}
-        animate={{ scale: 1, opacity: 1 }}
-        exit={{ scale: 0.92, opacity: 0 }}
-        transition={{ type: 'spring', stiffness: 320, damping: 28 }}
-        className="card"
-        style={{
-          width: '100%',
-          maxWidth: 440,
-          padding: 24,
-          backgroundColor: 'var(--color-surface)',
-        }}
-        onClick={(e) => e.stopPropagation()}
-        role="dialog"
-        aria-modal="true"
-        aria-label="Trigger discovery"
-      >
-        <div className="flex items-center justify-between mb-4">
-          <div className="flex items-center gap-2">
-            <Zap size={16} style={{ color: '#f59e0b' }} aria-hidden="true" />
-            <h3 className="font-semibold text-sm" style={{ color: 'var(--color-text)' }}>
-              Trigger Discovery
-            </h3>
-          </div>
+    <ResizableModal
+      modalId="onboard-trigger-discovery"
+      title={
+        <span className="flex items-center gap-2">
+          <Zap size={16} style={{ color: '#f59e0b' }} aria-hidden="true" />
+          Trigger Discovery
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={440}
+      footer={
+        <>
           <button
             type="button"
             onClick={onClose}
             className="btn btn-ghost btn-sm"
-            aria-label="Close dialog"
-            style={{ padding: '0.25rem' }}
           >
-            <X size={14} aria-hidden="true" />
+            Cancel
           </button>
+          <button
+            type="button"
+            className="btn btn-primary btn-sm"
+            disabled={!topic.trim()}
+            onClick={(e) => handleSubmit(e as unknown as React.FormEvent)}
+          >
+            <Zap size={13} aria-hidden="true" />
+            Trigger
+          </button>
+        </>
+      }
+    >
+      <p className="text-xs mb-4" style={{ color: 'var(--color-text-muted)' }}>
+        Enter a topic to trigger the onboard agent. It will discover relevant non-profits and
+        add them to the discovery queue.
+      </p>
+
+      <form onSubmit={handleSubmit} className="flex flex-col gap-3">
+        <div>
+          <label
+            htmlFor="trigger-topic"
+            className="text-xs font-medium mb-1 block"
+            style={{ color: 'var(--color-text-secondary)' }}
+          >
+            Topic
+          </label>
+          <input
+            id="trigger-topic"
+            type="text"
+            value={topic}
+            onChange={(e) => setTopic(e.target.value)}
+            placeholder='e.g. "Climate Action", "Education Policy"'
+            className="input"
+            style={{ fontSize: '0.85rem' }}
+            autoFocus
+            required
+          />
         </div>
-
-        <p className="text-xs mb-4" style={{ color: 'var(--color-text-muted)' }}>
-          Enter a topic to trigger the onboard agent. It will discover relevant non-profits and
-          add them to the discovery queue.
-        </p>
-
-        <form onSubmit={handleSubmit} className="flex flex-col gap-3">
-          <div>
-            <label
-              htmlFor="trigger-topic"
-              className="text-xs font-medium mb-1 block"
-              style={{ color: 'var(--color-text-secondary)' }}
-            >
-              Topic
-            </label>
-            <input
-              id="trigger-topic"
-              type="text"
-              value={topic}
-              onChange={(e) => setTopic(e.target.value)}
-              placeholder='e.g. "Climate Action", "Education Policy"'
-              className="input"
-              style={{ fontSize: '0.85rem' }}
-              autoFocus
-              required
-            />
-          </div>
-
-          <div className="flex items-center justify-end gap-2 mt-1">
-            <button
-              type="button"
-              onClick={onClose}
-              className="btn btn-ghost btn-sm"
-            >
-              Cancel
-            </button>
-            <button
-              type="submit"
-              className="btn btn-primary btn-sm"
-              disabled={!topic.trim()}
-            >
-              <Zap size={13} aria-hidden="true" />
-              Trigger
-            </button>
-          </div>
-        </form>
-      </motion.div>
-    </motion.div>
+        {/* Hidden submit for Enter-key */}
+        <button type="submit" style={{ display: 'none' }} aria-hidden="true" tabIndex={-1} />
+      </form>
+    </ResizableModal>
   );
 }
 
@@ -710,37 +672,46 @@ function SignupFormModal({
   }
 
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 100,
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-        background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
-      }}
-      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
+    <ResizableModal
+      modalId="onboard-nonprofit-signup"
+      title={
+        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <ClipboardList size={20} color="var(--color-primary)" />
+          Nonprofit Signup
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={640}
+      footer={
+        <>
+          {step > 1 && (
+            <button className="btn btn-secondary" onClick={() => setStep(step - 1)} style={{ marginRight: 'auto' }}>
+              <ChevronLeft size={14} /> Back
+            </button>
+          )}
+          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
+          {step < totalSteps ? (
+            <button className="btn btn-primary" onClick={() => setStep(step + 1)}>
+              Next <ChevronRight size={14} />
+            </button>
+          ) : (
+            <button
+              className="btn btn-primary"
+              onClick={handleSubmit}
+              disabled={submitting || !orgName.trim()}
+              style={{ display: 'flex', alignItems: 'center', gap: 6 }}
+            >
+              {submitting ? (
+                <><span className="spinner" style={{ width: 14, height: 14 }} /> Registering...</>
+              ) : (
+                <><CheckCircle2 size={14} /> Complete Signup</>
+              )}
+            </button>
+          )}
+        </>
+      }
     >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        exit={{ opacity: 0, scale: 0.95 }}
-        style={{
-          background: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          width: '100%', maxWidth: 640, maxHeight: '90vh',
-          overflow: 'auto', padding: 24,
-        }}
-      >
-        {/* Header */}
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
-          <h2 style={{ fontSize: 17, fontWeight: 700, color: 'var(--color-text)', display: 'flex', alignItems: 'center', gap: 8 }}>
-            <ClipboardList size={20} color="var(--color-primary)" />
-            Nonprofit Signup
-          </h2>
-          <button onClick={onClose} className="btn btn-ghost" style={{ padding: 4 }}>
-            <X size={18} />
-          </button>
-        </div>
-
+      <>
         {/* Progress */}
         <div style={{ display: 'flex', gap: 4, marginBottom: 20 }}>
           {Array.from({ length: totalSteps }).map((_, i) => (
@@ -988,39 +959,8 @@ function SignupFormModal({
           </div>
         )}
 
-        {/* Navigation */}
-        <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 20 }}>
-          <div>
-            {step > 1 && (
-              <button className="btn btn-secondary" onClick={() => setStep(step - 1)}>
-                <ChevronLeft size={14} /> Back
-              </button>
-            )}
-          </div>
-          <div style={{ display: 'flex', gap: 8 }}>
-            <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
-            {step < totalSteps ? (
-              <button className="btn btn-primary" onClick={() => setStep(step + 1)}>
-                Next <ChevronRight size={14} />
-              </button>
-            ) : (
-              <button
-                className="btn btn-primary"
-                onClick={handleSubmit}
-                disabled={submitting || !orgName.trim()}
-                style={{ display: 'flex', alignItems: 'center', gap: 6 }}
-              >
-                {submitting ? (
-                  <><span className="spinner" style={{ width: 14, height: 14 }} /> Registering...</>
-                ) : (
-                  <><CheckCircle2 size={14} /> Complete Signup</>
-                )}
-              </button>
-            )}
-          </div>
-        </div>
-      </motion.div>
-    </div>
+      </>
+    </ResizableModal>
   );
 }
 
diff --git a/components/outreach/OutreachPlaybookTab.tsx b/components/outreach/OutreachPlaybookTab.tsx
index ee9f6a9..c4026a9 100644
--- a/components/outreach/OutreachPlaybookTab.tsx
+++ b/components/outreach/OutreachPlaybookTab.tsx
@@ -12,6 +12,7 @@ import {
   Briefcase, GraduationCap, Scale, Megaphone,
   Video, PhoneCall, UserCheck, Pause, XCircle, Zap, Info,
 } from 'lucide-react';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ═══════════════════════════════════════════════════════════════════════════
    TYPES
@@ -722,14 +723,31 @@ function AddContactModal({ onClose, onAdded }: { onClose: () => void; onAdded: (
   }
 
   return (
-    <ModalOverlay onClose={onClose}>
-      <div style={{ maxWidth: 560, width: '100%', maxHeight: '90vh', overflow: 'auto' }}>
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
-          <h2 style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>Add Pipeline Contact</h2>
-          <button onClick={onClose} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--color-text-muted)' }}>
-            <X size={18} />
+    <ResizableModal
+      modalId="outreach-add-pipeline-contact"
+      title="Add Pipeline Contact"
+      onClose={onClose}
+      defaultWidth={560}
+      footer={
+        <>
+          <button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}>
+            Cancel
           </button>
-        </div>
+          <button
+            type="button"
+            disabled={saving}
+            onClick={(e) => handleSubmit(e as unknown as React.FormEvent)}
+            style={{
+              padding: '8px 20px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: 13, fontWeight: 600,
+              background: 'linear-gradient(135deg, #10b981, #059669)', color: '#fff', opacity: saving ? 0.7 : 1,
+            }}
+          >
+            {saving ? 'Saving...' : 'Add Contact'}
+          </button>
+        </>
+      }
+    >
+      <>
         <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '20px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
           <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
           <span>Add a new outreach contact for your playbook campaigns. Select the target type (official, coalition, media) and fill in location and channel details to enable targeted outreach tracking.</span>
@@ -817,21 +835,11 @@ function AddContactModal({ onClose, onAdded }: { onClose: () => void; onAdded: (
           <FormField label="Notes" full>
             <textarea value={form.notes} onChange={e => setForm({ ...form, notes: e.target.value })} rows={3} style={{ ...inputStyle, resize: 'vertical' }} placeholder="Any context for this contact..." />
           </FormField>
-
-          <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
-            <button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}>
-              Cancel
-            </button>
-            <button type="submit" disabled={saving} style={{
-              padding: '8px 20px', borderRadius: 8, border: 'none', cursor: 'pointer', fontSize: 13, fontWeight: 600,
-              background: 'linear-gradient(135deg, #10b981, #059669)', color: '#fff', opacity: saving ? 0.7 : 1,
-            }}>
-              {saving ? 'Saving...' : 'Add Contact'}
-            </button>
-          </div>
+          {/* Hidden submit for Enter-key */}
+          <button type="submit" style={{ display: 'none' }} aria-hidden="true" tabIndex={-1} />
         </form>
-      </div>
-    </ModalOverlay>
+      </>
+    </ResizableModal>
   );
 }
 
@@ -981,99 +989,98 @@ function TemplatesView() {
       )}
 
       {/* Generate modal */}
-      <AnimatePresence>
-        {showGenerateModal && (
-          <ModalOverlay onClose={() => { setShowGenerateModal(false); setGeneratedLetter(null); }}>
-            <div style={{ maxWidth: 640, width: '100%', maxHeight: '90vh', overflow: 'auto' }}>
-              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
-                <h2 style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)', display: 'flex', alignItems: 'center', gap: 8 }}>
-                  <Sparkles size={18} style={{ color: '#f59e0b' }} /> Generate Letter
-                </h2>
-                <button onClick={() => { setShowGenerateModal(false); setGeneratedLetter(null); }} style={{ border: 'none', background: 'none', cursor: 'pointer', color: 'var(--color-text-muted)' }}>
-                  <X size={18} />
+      {showGenerateModal && (
+        <ResizableModal
+          modalId="outreach-generate-letter"
+          title={
+            <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+              <Sparkles size={18} style={{ color: '#f59e0b' }} /> Generate Letter
+            </span>
+          }
+          onClose={() => { setShowGenerateModal(false); setGeneratedLetter(null); }}
+          defaultWidth={640}
+          footer={
+            !generatedLetter ? (
+              <>
+                <button onClick={() => setShowGenerateModal(false)} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}>
+                  Cancel
                 </button>
-              </div>
-
-              {!generatedLetter ? (
-                <>
-                  <FormField label="Select Pipeline Contact" full>
-                    <select value={selectedEntryId} onChange={e => setSelectedEntryId(e.target.value)} style={inputStyle}>
-                      <option value="">Choose a contact...</option>
-                      {pipelineEntries.map(e => (
-                        <option key={e.id} value={e.id}>{e.office_name}{e.official_name ? ` (${e.official_name})` : ''} - {e.state || 'N/A'}</option>
-                      ))}
-                    </select>
-                  </FormField>
-                  <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 16 }}>
-                    <button onClick={() => setShowGenerateModal(false)} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}>
-                      Cancel
-                    </button>
-                    <button
-                      onClick={generateLetter}
-                      disabled={!selectedEntryId || generating}
-                      style={{
-                        display: 'flex', alignItems: 'center', gap: 6,
-                        padding: '8px 20px', borderRadius: 8, border: 'none', cursor: 'pointer',
-                        fontSize: 13, fontWeight: 600,
-                        background: 'linear-gradient(135deg, #f59e0b, #d97706)', color: '#fff',
-                        opacity: (!selectedEntryId || generating) ? 0.6 : 1,
-                      }}
-                    >
-                      {generating ? <><RefreshCw size={14} className="animate-spin" /> Generating...</> : <><Sparkles size={14} /> Generate</>}
-                    </button>
-                  </div>
-                </>
-              ) : (
-                <>
-                  {generatedLetter.subject && (
-                    <div style={{ marginBottom: 12 }}>
-                      <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>SUBJECT</label>
-                      <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', padding: 8, borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
-                        {generatedLetter.subject}
-                      </div>
-                    </div>
-                  )}
-                  <div>
-                    <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>BODY</label>
-                    <div style={{ fontSize: 13, color: 'var(--color-text)', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', whiteSpace: 'pre-wrap', lineHeight: 1.6, maxHeight: 400, overflow: 'auto' }}>
-                      {generatedLetter.body}
-                    </div>
-                  </div>
-                  {generatedLetter.notes && (
-                    <div style={{ marginTop: 12, fontSize: 12, color: 'var(--color-text-muted)', padding: 8, borderRadius: 8, backgroundColor: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.2)' }}>
-                      <strong>AI Notes:</strong> {generatedLetter.notes}
-                    </div>
-                  )}
-                  <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
-                    <button
-                      onClick={() => {
-                        const full = generatedLetter.subject
-                          ? `Subject: ${generatedLetter.subject}\n\n${generatedLetter.body}`
-                          : generatedLetter.body;
-                        copyToClipboard(full);
-                      }}
-                      style={{
-                        display: 'flex', alignItems: 'center', gap: 6,
-                        padding: '8px 16px', borderRadius: 8, border: 'none', cursor: 'pointer',
-                        fontSize: 13, fontWeight: 600,
-                        background: 'linear-gradient(135deg, #10b981, #059669)', color: '#fff',
-                      }}
-                    >
-                      <Copy size={14} /> {copied ? 'Copied!' : 'Copy to Clipboard'}
-                    </button>
-                    <button
-                      onClick={() => setGeneratedLetter(null)}
-                      style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}
-                    >
-                      Generate Another
-                    </button>
+                <button
+                  onClick={generateLetter}
+                  disabled={!selectedEntryId || generating}
+                  style={{
+                    display: 'flex', alignItems: 'center', gap: 6,
+                    padding: '8px 20px', borderRadius: 8, border: 'none', cursor: 'pointer',
+                    fontSize: 13, fontWeight: 600,
+                    background: 'linear-gradient(135deg, #f59e0b, #d97706)', color: '#fff',
+                    opacity: (!selectedEntryId || generating) ? 0.6 : 1,
+                  }}
+                >
+                  {generating ? <><RefreshCw size={14} className="animate-spin" /> Generating...</> : <><Sparkles size={14} /> Generate</>}
+                </button>
+              </>
+            ) : (
+              <>
+                <button
+                  onClick={() => {
+                    const full = generatedLetter.subject
+                      ? `Subject: ${generatedLetter.subject}\n\n${generatedLetter.body}`
+                      : generatedLetter.body;
+                    copyToClipboard(full);
+                  }}
+                  style={{
+                    display: 'flex', alignItems: 'center', gap: 6,
+                    padding: '8px 16px', borderRadius: 8, border: 'none', cursor: 'pointer',
+                    fontSize: 13, fontWeight: 600,
+                    background: 'linear-gradient(135deg, #10b981, #059669)', color: '#fff',
+                  }}
+                >
+                  <Copy size={14} /> {copied ? 'Copied!' : 'Copy to Clipboard'}
+                </button>
+                <button
+                  onClick={() => setGeneratedLetter(null)}
+                  style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', cursor: 'pointer', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13 }}
+                >
+                  Generate Another
+                </button>
+              </>
+            )
+          }
+        >
+          {!generatedLetter ? (
+            <FormField label="Select Pipeline Contact" full>
+              <select value={selectedEntryId} onChange={e => setSelectedEntryId(e.target.value)} style={inputStyle}>
+                <option value="">Choose a contact...</option>
+                {pipelineEntries.map(e => (
+                  <option key={e.id} value={e.id}>{e.office_name}{e.official_name ? ` (${e.official_name})` : ''} - {e.state || 'N/A'}</option>
+                ))}
+              </select>
+            </FormField>
+          ) : (
+            <>
+              {generatedLetter.subject && (
+                <div style={{ marginBottom: 12 }}>
+                  <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>SUBJECT</label>
+                  <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', padding: 8, borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)' }}>
+                    {generatedLetter.subject}
                   </div>
-                </>
+                </div>
               )}
-            </div>
-          </ModalOverlay>
-        )}
-      </AnimatePresence>
+              <div>
+                <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>BODY</label>
+                <div style={{ fontSize: 13, color: 'var(--color-text)', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)', border: '1px solid var(--color-border)', whiteSpace: 'pre-wrap', lineHeight: 1.6, maxHeight: 400, overflow: 'auto' }}>
+                  {generatedLetter.body}
+                </div>
+              </div>
+              {generatedLetter.notes && (
+                <div style={{ marginTop: 12, fontSize: 12, color: 'var(--color-text-muted)', padding: 8, borderRadius: 8, backgroundColor: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.2)' }}>
+                  <strong>AI Notes:</strong> {generatedLetter.notes}
+                </div>
+              )}
+            </>
+          )}
+        </ResizableModal>
+      )}
     </>
   );
 }
diff --git a/components/partners/PartnersTab.tsx b/components/partners/PartnersTab.tsx
index ffa95d7..c1f5b1e 100644
--- a/components/partners/PartnersTab.tsx
+++ b/components/partners/PartnersTab.tsx
@@ -16,6 +16,7 @@ import { useDebounce } from '@/hooks/useDebounce';
 import { SkeletonList } from '../Skeleton';
 import { useClientSort, SortConfig } from '@/hooks/useClientSort';
 import SortDropdown from '../shared/SortDropdown';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 type PartnerType = 'nonprofit' | 'government' | 'corporation' | 'university' | 'media' | 'foundation' | 'legal' | 'other';
@@ -462,12 +463,6 @@ function AddPartnerModal({ onClose, onAdded }: { onClose: () => void; onAdded: (
   });
   const [saving, setSaving] = useState(false);
 
-  useEffect(() => {
-    const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
-    window.addEventListener('keydown', h);
-    return () => window.removeEventListener('keydown', h);
-  }, [onClose]);
-
   async function handleSubmit(e: React.FormEvent) {
     e.preventDefault();
     if (!form.name.trim()) return;
@@ -487,61 +482,70 @@ function AddPartnerModal({ onClose, onAdded }: { onClose: () => void; onAdded: (
   const inp: React.CSSProperties = { width: '100%', padding: '8px 10px', borderRadius: 8, border: '1px solid var(--color-border)', backgroundColor: 'var(--color-surface-el)', color: 'var(--color-text)', fontSize: 13 };
 
   return (
-    <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.65)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }} onClick={onClose}>
-      <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
-        style={{ backgroundColor: 'var(--color-surface)', borderRadius: 16, border: '1px solid var(--color-border)', padding: 24, width: '100%', maxWidth: 540, maxHeight: '82vh', overflowY: 'auto' }}
-        onClick={e => e.stopPropagation()}>
-        <h3 style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)', margin: '0 0 12px' }}>Add Partner / Prospect</h3>
-        <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '16px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
-          <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
-          <span>Register a new partner or prospect organization. Choose whether to add them as a current partner or place them in the vetting queue for review before formalizing the relationship.</span>
+    <ResizableModal
+      modalId="partners-add"
+      title="Add Partner / Prospect"
+      onClose={onClose}
+      defaultWidth={540}
+      footer={
+        <>
+          <button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13, cursor: 'pointer' }}>Cancel</button>
+          <button
+            type="button"
+            disabled={saving || !form.name.trim()}
+            onClick={(e) => handleSubmit(e as unknown as React.FormEvent)}
+            style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: form.is_prospect ? '#6366f1' : '#10b981', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer', opacity: saving ? 0.7 : 1 }}
+          >
+            {saving ? 'Saving…' : form.is_prospect ? 'Add to Vetting' : 'Add Partner'}
+          </button>
+        </>
+      }
+    >
+      <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: '16px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
+        <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
+        <span>Register a new partner or prospect organization. Choose whether to add them as a current partner or place them in the vetting queue for review before formalizing the relationship.</span>
+      </div>
+      <form onSubmit={handleSubmit}>
+        <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
+          {[false, true].map(val => (
+            <button key={String(val)} type="button" onClick={() => setForm(f => ({ ...f, is_prospect: val }))}
+              style={{ flex: 1, padding: '8px', borderRadius: 8, border: `1.5px solid ${form.is_prospect === val ? '#6366f1' : 'var(--color-border)'}`, backgroundColor: form.is_prospect === val ? 'rgba(99,102,241,0.12)' : 'transparent', color: form.is_prospect === val ? '#818cf8' : 'var(--color-text-secondary)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
+              {val ? '🔍 Add to Vetting Queue' : '🤝 Add as Current Partner'}
+            </button>
+          ))}
         </div>
-        <form onSubmit={handleSubmit}>
-          <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
-            {[false, true].map(val => (
-              <button key={String(val)} type="button" onClick={() => setForm(f => ({ ...f, is_prospect: val }))}
-                style={{ flex: 1, padding: '8px', borderRadius: 8, border: `1.5px solid ${form.is_prospect === val ? '#6366f1' : 'var(--color-border)'}`, backgroundColor: form.is_prospect === val ? 'rgba(99,102,241,0.12)' : 'transparent', color: form.is_prospect === val ? '#818cf8' : 'var(--color-text-secondary)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
-                {val ? '🔍 Add to Vetting Queue' : '🤝 Add as Current Partner'}
-              </button>
-            ))}
+        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
+          <div>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Type *</label>
+            <select value={form.partner_type} onChange={e => setForm(f => ({ ...f, partner_type: e.target.value as PartnerType }))} style={inp}>
+              {Object.entries(TYPE_CONFIG).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
+            </select>
           </div>
-          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
-            <div>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Type *</label>
-              <select value={form.partner_type} onChange={e => setForm(f => ({ ...f, partner_type: e.target.value as PartnerType }))} style={inp}>
-                {Object.entries(TYPE_CONFIG).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
-              </select>
-            </div>
-            <div>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Organization Name *</label>
-              <input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} style={inp} />
-            </div>
-            <div>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Contact Name</label>
-              <input value={form.contact_name} onChange={e => setForm(f => ({ ...f, contact_name: e.target.value }))} style={inp} />
-            </div>
-            <div>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Twitter Handle</label>
-              <input value={form.twitter_handle} onChange={e => setForm(f => ({ ...f, twitter_handle: e.target.value }))} placeholder="@handle" style={inp} />
-            </div>
-            <div style={{ gridColumn: '1 / -1' }}>
-              <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Website</label>
-              <input value={form.website_url} onChange={e => setForm(f => ({ ...f, website_url: e.target.value }))} placeholder="https://..." style={inp} />
-            </div>
+          <div>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Organization Name *</label>
+            <input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} style={inp} />
           </div>
-          <div style={{ marginBottom: 14 }}>
-            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Description</label>
-            <textarea value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} rows={2} style={{ ...inp, resize: 'vertical' as const }} />
+          <div>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Contact Name</label>
+            <input value={form.contact_name} onChange={e => setForm(f => ({ ...f, contact_name: e.target.value }))} style={inp} />
           </div>
-          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
-            <button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 8, border: '1px solid var(--color-border)', backgroundColor: 'transparent', color: 'var(--color-text-secondary)', fontSize: 13, cursor: 'pointer' }}>Cancel</button>
-            <button type="submit" disabled={saving || !form.name.trim()} style={{ padding: '8px 16px', borderRadius: 8, border: 'none', backgroundColor: form.is_prospect ? '#6366f1' : '#10b981', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer', opacity: saving ? 0.7 : 1 }}>
-              {saving ? 'Saving…' : form.is_prospect ? 'Add to Vetting' : 'Add Partner'}
-            </button>
+          <div>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Twitter Handle</label>
+            <input value={form.twitter_handle} onChange={e => setForm(f => ({ ...f, twitter_handle: e.target.value }))} placeholder="@handle" style={inp} />
           </div>
-        </form>
-      </motion.div>
-    </div>
+          <div style={{ gridColumn: '1 / -1' }}>
+            <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Website</label>
+            <input value={form.website_url} onChange={e => setForm(f => ({ ...f, website_url: e.target.value }))} placeholder="https://..." style={inp} />
+          </div>
+        </div>
+        <div style={{ marginBottom: 14 }}>
+          <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--color-text-muted)', display: 'block', marginBottom: 4 }}>Description</label>
+          <textarea value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} rows={2} style={{ ...inp, resize: 'vertical' as const }} />
+        </div>
+        {/* Hidden submit for Enter-key */}
+        <button type="submit" style={{ display: 'none' }} aria-hidden="true" tabIndex={-1} />
+      </form>
+    </ResizableModal>
   );
 }
 
diff --git a/components/petitions/AddPetitionModal.tsx b/components/petitions/AddPetitionModal.tsx
index 1336179..c268b94 100644
--- a/components/petitions/AddPetitionModal.tsx
+++ b/components/petitions/AddPetitionModal.tsx
@@ -1,13 +1,13 @@
 'use client';
 
 import { useState, useEffect, useRef } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
 import {
   X, Plus, Send, CheckCircle, AlertCircle, Loader2,
-  Link2, Users, Globe, Facebook, Instagram, Printer, Bell, Zap,
-  Copy, Share2, ExternalLink, ChevronDown, ChevronUp, Info,
+  Link2, Users, Globe, Printer, Bell, Zap,
+  Copy, Share2, ExternalLink, Info,
 } from 'lucide-react';
 import type { Petition } from './PetitionCard';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 interface AddPetitionModalProps {
@@ -313,92 +313,89 @@ export default function AddPetitionModal({
     }
   }
 
+  if (!isOpen) return null;
+
   return (
-    <AnimatePresence>
-      {isOpen && (
+    <ResizableModal
+      modalId={isEditMode ? 'petition-edit' : 'petition-create'}
+      title={
+        <span className="flex items-center gap-2">
+          <Plus size={14} style={{ color: 'var(--color-primary)' }} aria-hidden="true" />
+          {isEditMode ? 'Edit Petition' : 'Add Petition'}
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={560}
+      footer={
         <>
-          {/* Backdrop */}
-          <motion.div
-            key="backdrop"
-            initial={{ opacity: 0 }}
-            animate={{ opacity: 1 }}
-            exit={{ opacity: 0 }}
-            transition={{ duration: 0.18 }}
+          <button
+            type="button"
             onClick={onClose}
-            style={{
-              position: 'fixed',
-              inset: 0,
-              backgroundColor: 'rgba(0,0,0,0.65)',
-              backdropFilter: 'blur(3px)',
-              zIndex: 40,
-            }}
-            aria-hidden="true"
-          />
+            disabled={submitting || publishing}
+            className="btn btn-secondary"
+          >
+            {publishStep === 'done' ? 'Close' : 'Cancel'}
+          </button>
 
-          {/* Modal panel */}
-          <motion.div
-            key="modal"
-            initial={{ opacity: 0, scale: 0.96, y: 12 }}
-            animate={{ opacity: 1, scale: 1, y: 0 }}
-            exit={{ opacity: 0, scale: 0.96, y: 12 }}
-            transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
-            role="dialog"
-            aria-modal="true"
-            aria-label={isEditMode ? 'Edit petition' : 'Add petition'}
-            style={{
-              position: 'fixed',
-              inset: 0,
-              display: 'flex',
-              alignItems: 'flex-start',
-              justifyContent: 'center',
-              zIndex: 50,
-              padding: '1rem',
-              paddingTop: 'clamp(1rem, 8vh, 5rem)',
-              overflowY: 'auto',
-            }}
+          {/* Save only button */}
+          <button
+            type="button"
+            onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
+            disabled={submitting || publishing}
+            className="btn btn-ghost"
+            style={{ border: '1px solid var(--color-border)' }}
           >
-            <div
+            {submitting ? (
+              <>
+                <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
+                {isEditMode ? 'Saving...' : 'Save Only'}
+              </>
+            ) : isEditMode ? (
+              'Save Changes'
+            ) : (
+              'Save Only'
+            )}
+          </button>
+
+          {/* Create & Publish Now button (only for new petitions with platforms selected) */}
+          {!isEditMode && (
+            <button
+              type="button"
+              onClick={(e) => handlePublishNow(e as unknown as React.FormEvent)}
+              disabled={submitting || publishing || selectedPlatforms.size === 0 || publishStep === 'done'}
+              className="btn btn-primary"
               style={{
-                backgroundColor: 'var(--color-surface)',
-                border: '1px solid var(--color-border)',
-                borderRadius: 'var(--radius-lg)',
-                width: '100%',
-                maxWidth: 560,
-                boxShadow: '0 24px 60px rgba(0,0,0,0.5)',
+                opacity: selectedPlatforms.size === 0 ? 0.5 : 1,
+                background: selectedPlatforms.size > 0 ? 'linear-gradient(135deg, var(--color-primary), #7c3aed)' : undefined,
               }}
             >
-              {/* Header */}
-              <div
-                className="flex items-center justify-between px-5 py-4"
-                style={{ borderBottom: '1px solid var(--color-border)' }}
-              >
-                <div className="flex items-center gap-2">
-                  <Plus
-                    size={16}
-                    style={{ color: 'var(--color-primary)' }}
-                    aria-hidden="true"
-                  />
-                  <h2
-                    className="font-semibold text-sm"
-                    style={{ color: 'var(--color-text)' }}
-                    id="modal-title"
-                  >
-                    {isEditMode ? 'Edit Petition' : 'Add Petition'}
-                  </h2>
-                </div>
-                <button
-                  type="button"
-                  onClick={onClose}
-                  className="btn btn-ghost btn-sm"
-                  aria-label="Close modal"
-                >
-                  <X size={15} aria-hidden="true" />
-                </button>
-              </div>
-
-              {/* Form */}
-              <form onSubmit={handleSubmit} noValidate>
-                <div className="px-5 py-4 flex flex-col gap-4">
+              {publishing ? (
+                <>
+                  <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
+                  Publishing...
+                </>
+              ) : publishStep === 'done' ? (
+                <>
+                  <CheckCircle size={15} aria-hidden="true" />
+                  Published!
+                </>
+              ) : (
+                <>
+                  <Send size={15} aria-hidden="true" />
+                  Create & Publish ({selectedPlatforms.size})
+                </>
+              )}
+            </button>
+          )}
+        </>
+      }
+    >
+      <form
+        onSubmit={handleSubmit}
+        noValidate
+        aria-label={isEditMode ? 'Edit petition' : 'Add petition'}
+      >
+        <div className="flex flex-col gap-4">
                   {/* Usage guidance */}
                   <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
                     <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
@@ -651,7 +648,7 @@ export default function AddPetitionModal({
 
                 {/* Publish Platforms (only for new petitions) */}
                 {!isEditMode && (
-                  <div className="px-5 pb-2">
+                  <div className="pb-2">
                     <div className="flex items-center justify-between mb-2">
                       <label className="text-xs font-medium" style={{ color: 'var(--color-text-secondary)' }}>
                         Publish Now To
@@ -729,76 +726,8 @@ export default function AddPetitionModal({
                   <PromoteShareSection petitionUrl={form.url} petitionTitle={form.title} />
                 )}
 
-                {/* Footer */}
-                <div
-                  className="flex items-center justify-end gap-2 px-5 py-4"
-                  style={{ borderTop: '1px solid var(--color-border)' }}
-                >
-                  <button
-                    type="button"
-                    onClick={onClose}
-                    disabled={submitting || publishing}
-                    className="btn btn-secondary"
-                  >
-                    {publishStep === 'done' ? 'Close' : 'Cancel'}
-                  </button>
-
-                  {/* Save only button */}
-                  <button
-                    type="submit"
-                    disabled={submitting || publishing}
-                    className="btn btn-ghost"
-                    style={{ border: '1px solid var(--color-border)' }}
-                  >
-                    {submitting ? (
-                      <>
-                        <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
-                        {isEditMode ? 'Saving...' : 'Save Only'}
-                      </>
-                    ) : isEditMode ? (
-                      'Save Changes'
-                    ) : (
-                      'Save Only'
-                    )}
-                  </button>
-
-                  {/* Create & Publish Now button (only for new petitions with platforms selected) */}
-                  {!isEditMode && (
-                    <button
-                      type="button"
-                      onClick={handlePublishNow}
-                      disabled={submitting || publishing || selectedPlatforms.size === 0 || publishStep === 'done'}
-                      className="btn btn-primary"
-                      style={{
-                        opacity: selectedPlatforms.size === 0 ? 0.5 : 1,
-                        background: selectedPlatforms.size > 0 ? 'linear-gradient(135deg, var(--color-primary), #7c3aed)' : undefined,
-                      }}
-                    >
-                      {publishing ? (
-                        <>
-                          <span className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} aria-hidden="true" />
-                          Publishing...
-                        </>
-                      ) : publishStep === 'done' ? (
-                        <>
-                          <CheckCircle size={15} aria-hidden="true" />
-                          Published!
-                        </>
-                      ) : (
-                        <>
-                          <Send size={15} aria-hidden="true" />
-                          Create & Publish ({selectedPlatforms.size})
-                        </>
-                      )}
-                    </button>
-                  )}
-                </div>
-              </form>
-            </div>
-          </motion.div>
-        </>
-      )}
-    </AnimatePresence>
+      </form>
+    </ResizableModal>
   );
 }
 
diff --git a/components/petitions/PlatformPreview.tsx b/components/petitions/PlatformPreview.tsx
index 628463b..2404793 100644
--- a/components/petitions/PlatformPreview.tsx
+++ b/components/petitions/PlatformPreview.tsx
@@ -33,6 +33,7 @@ import {
   Check,
   AlertTriangle,
 } from 'lucide-react';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 export interface PlatformPreviewPetition {
@@ -1026,6 +1027,7 @@ function POAPreview({
 /* ─── Sub-component: Confirmation Dialog ─────────────────────────────────── */
 interface ConfirmDialogProps {
   isOpen: boolean;
+  modalId: string;
   title: string;
   message: string;
   confirmLabel: string;
@@ -1036,6 +1038,7 @@ interface ConfirmDialogProps {
 
 function ConfirmDialog({
   isOpen,
+  modalId,
   title,
   message,
   confirmLabel,
@@ -1043,94 +1046,44 @@ function ConfirmDialog({
   onConfirm,
   onCancel,
 }: ConfirmDialogProps) {
+  if (!isOpen) return null;
   return (
-    <AnimatePresence>
-      {isOpen && (
+    <ResizableModal
+      modalId={modalId}
+      title={title}
+      onClose={onCancel}
+      defaultWidth={400}
+      footer={
         <>
-          <motion.div
-            key="confirm-backdrop"
-            initial={{ opacity: 0 }}
-            animate={{ opacity: 1 }}
-            exit={{ opacity: 0 }}
-            transition={{ duration: 0.15 }}
+          <button
+            type="button"
             onClick={onCancel}
-            style={{
-              position: 'fixed',
-              inset: 0,
-              backgroundColor: 'rgba(0,0,0,0.55)',
-              backdropFilter: 'blur(2px)',
-              zIndex: 60,
-            }}
-            aria-hidden="true"
-          />
-          <motion.div
-            key="confirm-dialog"
-            initial={{ opacity: 0, scale: 0.95, y: 8 }}
-            animate={{ opacity: 1, scale: 1, y: 0 }}
-            exit={{ opacity: 0, scale: 0.95, y: 8 }}
-            transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }}
-            role="alertdialog"
-            aria-modal="true"
-            aria-labelledby="confirm-title"
-            aria-describedby="confirm-msg"
-            style={{
-              position: 'fixed',
-              top: '50%',
-              left: '50%',
-              transform: 'translate(-50%, -50%)',
-              zIndex: 70,
-              width: '100%',
-              maxWidth: 380,
-              backgroundColor: 'var(--color-surface)',
-              border: '1px solid var(--color-border)',
-              borderRadius: 'var(--radius-lg)',
-              padding: '1.25rem',
-              boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
-            }}
+            className="btn btn-secondary btn-sm"
           >
-            <h3
-              id="confirm-title"
-              style={{
-                color: 'var(--color-text)',
-                fontWeight: 600,
-                fontSize: '0.9375rem',
-                marginBottom: '0.5rem',
-              }}
-            >
-              {title}
-            </h3>
-            <p
-              id="confirm-msg"
-              style={{
-                color: 'var(--color-text-secondary)',
-                fontSize: '0.875rem',
-                lineHeight: 1.5,
-                marginBottom: '1rem',
-              }}
-            >
-              {message}
-            </p>
-            <div className="flex justify-end gap-2">
-              <button
-                type="button"
-                onClick={onCancel}
-                className="btn btn-secondary btn-sm"
-              >
-                Cancel
-              </button>
-              <button
-                type="button"
-                onClick={onConfirm}
-                className="btn btn-sm"
-                style={confirmStyle}
-              >
-                {confirmLabel}
-              </button>
-            </div>
-          </motion.div>
+            Cancel
+          </button>
+          <button
+            type="button"
+            onClick={onConfirm}
+            className="btn btn-sm"
+            style={confirmStyle}
+          >
+            {confirmLabel}
+          </button>
         </>
-      )}
-    </AnimatePresence>
+      }
+    >
+      <p
+        id="confirm-msg"
+        style={{
+          color: 'var(--color-text-secondary)',
+          fontSize: '0.875rem',
+          lineHeight: 1.5,
+        }}
+      >
+        {message}
+      </p>
+    </ResizableModal>
   );
 }
 
@@ -1608,6 +1561,7 @@ export default function PlatformPreview({
       {/* ── Confirmation Dialogs ──────────────────────────────────────────── */}
       <ConfirmDialog
         isOpen={confirmTarget === 'save'}
+        modalId="platform-preview-save-confirm"
         title="Save as Draft?"
         message={`"${petition.title}" will be saved as a draft petition and can be edited or published later.`}
         confirmLabel="Save Draft"
@@ -1622,6 +1576,7 @@ export default function PlatformPreview({
 
       <ConfirmDialog
         isOpen={confirmTarget === 'publish'}
+        modalId="platform-preview-publish-confirm"
         title="Publish to Pulse of America?"
         message={`"${petition.title}" will be published as an active petition on the Pulse of America platform and will be publicly visible immediately.`}
         confirmLabel="Publish Now"
diff --git a/components/pipeline/ScheduleModal.tsx b/components/pipeline/ScheduleModal.tsx
index 470e6cc..ddea939 100644
--- a/components/pipeline/ScheduleModal.tsx
+++ b/components/pipeline/ScheduleModal.tsx
@@ -1,9 +1,9 @@
 'use client';
 
 import { useState } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { CalendarClock, X, Loader2, Check, Info } from 'lucide-react';
+import { CalendarClock, Loader2, Check, Info } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
+import ResizableModal from '@/components/shared/ResizableModal';
 
 const PLATFORMS = [
   { id: 'twitter',  label: 'Twitter / X',  color: '#1d9bf0' },
@@ -85,48 +85,39 @@ export default function ScheduleModal({ open, onClose, contentId, contentType, t
     }
   }
 
+  if (!open) return null;
+
   return (
-    <AnimatePresence>
-      {open && (
-        <motion.div
-          initial={{ opacity: 0 }}
-          animate={{ opacity: 1 }}
-          exit={{ opacity: 0 }}
-          style={{
-            position: 'fixed', inset: 0, zIndex: 100,
-            display: 'flex', alignItems: 'center', justifyContent: 'center',
-            backgroundColor: 'rgba(0,0,0,0.6)',
-          }}
-          onClick={onClose}
-        >
-          <motion.div
-            initial={{ scale: 0.95, opacity: 0 }}
-            animate={{ scale: 1, opacity: 1 }}
-            exit={{ scale: 0.95, opacity: 0 }}
-            onClick={e => e.stopPropagation()}
-            style={{
-              backgroundColor: 'var(--color-surface)',
-              border: '1px solid var(--color-border)',
-              borderRadius: 16,
-              padding: 24,
-              width: '100%',
-              maxWidth: 440,
-              boxShadow: '0 12px 48px rgba(0,0,0,0.5)',
-            }}
+    <ResizableModal
+      modalId="pipeline-schedule"
+      title={
+        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <CalendarClock size={16} style={{ color: 'var(--color-primary)' }} />
+          Schedule Dispatch
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={440}
+      footer={
+        <>
+          <button className="btn btn-ghost btn-sm" onClick={onClose}>Cancel</button>
+          <button
+            className="btn btn-primary btn-sm"
+            onClick={handleSchedule}
+            disabled={submitting || done || selected.size === 0}
+            style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 120 }}
           >
-            {/* Header */}
-            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
-                <CalendarClock size={18} style={{ color: 'var(--color-primary)' }} />
-                <h3 style={{ fontSize: 15, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
-                  Schedule Dispatch
-                </h3>
-              </div>
-              <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-text-muted)', padding: 4 }}>
-                <X size={16} />
-              </button>
-            </div>
-
+            {submitting ? (
+              <><Loader2 size={14} className="spinner" /> Scheduling...</>
+            ) : done ? (
+              <><Check size={14} /> Scheduled!</>
+            ) : (
+              <><CalendarClock size={14} /> Schedule</>
+            )}
+          </button>
+        </>
+      }
+    >
             {/* Usage guidance */}
             <div style={{ backgroundColor: 'rgba(16, 185, 129, 0.08)', borderLeft: '3px solid var(--color-primary)', borderRadius: 'var(--radius-sm)', padding: '10px 14px', marginBottom: 16, fontSize: '13px', color: 'var(--color-text-secondary)', lineHeight: '1.5', display: 'flex', gap: 8, alignItems: 'flex-start' }}>
               <Info size={14} style={{ color: 'var(--color-primary)', flexShrink: 0, marginTop: 1 }} />
@@ -189,27 +180,6 @@ export default function ScheduleModal({ open, onClose, contentId, contentType, t
               />
             </div>
 
-            {/* Actions */}
-            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
-              <button className="btn btn-ghost btn-sm" onClick={onClose}>Cancel</button>
-              <button
-                className="btn btn-primary btn-sm"
-                onClick={handleSchedule}
-                disabled={submitting || done || selected.size === 0}
-                style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 120 }}
-              >
-                {submitting ? (
-                  <><Loader2 size={14} className="spinner" /> Scheduling...</>
-                ) : done ? (
-                  <><Check size={14} /> Scheduled!</>
-                ) : (
-                  <><CalendarClock size={14} /> Schedule</>
-                )}
-              </button>
-            </div>
-          </motion.div>
-        </motion.div>
-      )}
-    </AnimatePresence>
+    </ResizableModal>
   );
 }
diff --git a/components/settings/ApiCredentialsSection.tsx b/components/settings/ApiCredentialsSection.tsx
index 0c86922..4971c3d 100644
--- a/components/settings/ApiCredentialsSection.tsx
+++ b/components/settings/ApiCredentialsSection.tsx
@@ -2,12 +2,13 @@
 
 import { useState, useEffect, useCallback } from 'react';
 import {
-  Key, Shield, Plus, Trash2, X, RefreshCw, Copy, Check,
+  Key, Shield, Plus, Trash2, RefreshCw, Copy, Check,
   ChevronDown, ChevronRight, AlertTriangle, CheckCircle2, XCircle,
   Clock, Wifi, WifiOff, Eye, EyeOff, Globe, Mail, Unplug, Plug,
   MessageSquare, Cloud, Camera, Zap,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
+import ResizableModal from '../shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -538,156 +539,121 @@ export default function ApiCredentialsSection() {
 
       {/* ── Generate Key Modal ────────────────────────────────────────────── */}
       {showGenerateModal && (
-        <div style={{
-          position: 'fixed', inset: 0, zIndex: 1000,
-          background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center',
-        }}>
-          <div className="card" style={{ padding: 24, width: 440, maxWidth: '90vw' }}>
-            {generatedKey ? (
-              /* ── Key Created — One-time display ── */
+        <ResizableModal
+          modalId={generatedKey ? 'api-key-generated' : 'api-key-create'}
+          title={generatedKey ? 'API Key Generated' : 'Generate API Key'}
+          onClose={closeGenerateModal}
+          defaultWidth={520}
+          footer={
+            generatedKey ? (
+              <button className="btn btn-primary" onClick={closeGenerateModal}>
+                Done
+              </button>
+            ) : (
               <>
-                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
-                  <CheckCircle2 size={20} style={{ color: 'var(--color-success)' }} />
-                  <h3 style={{ margin: 0, fontSize: 16 }}>API Key Generated</h3>
-                </div>
-
-                <div style={{
-                  background: 'var(--color-surface-el)', borderRadius: 'var(--radius-md)',
-                  padding: 12, marginBottom: 12, position: 'relative',
-                }}>
-                  <code style={{ fontSize: 12, wordBreak: 'break-all', lineHeight: 1.5 }}>
-                    {generatedKey}
-                  </code>
-                  <button
-                    className="btn btn-ghost btn-sm"
-                    onClick={copyKey}
-                    style={{ position: 'absolute', top: 8, right: 8 }}
-                  >
-                    {keyCopied ? <Check size={14} style={{ color: 'var(--color-success)' }} /> : <Copy size={14} />}
-                  </button>
-                </div>
-
-                <div style={{
-                  display: 'flex', alignItems: 'center', gap: 6, padding: 10,
-                  background: 'rgba(244, 63, 94, 0.1)', borderRadius: 'var(--radius-sm)',
-                  marginBottom: 16, fontSize: 12, color: 'var(--color-error)',
-                }}>
-                  <AlertTriangle size={14} />
-                  This key will not be shown again. Copy it now.
-                </div>
-
-                <button className="btn btn-primary" onClick={closeGenerateModal} style={{ width: '100%' }}>
-                  Done
+                <button className="btn btn-ghost" onClick={closeGenerateModal}>Cancel</button>
+                <button
+                  className="btn btn-primary"
+                  onClick={handleGenerateKey}
+                  disabled={generating || !newKeyName.trim()}
+                >
+                  {generating ? <><RefreshCw size={14} className="spinner" /> Generating...</> : <><Key size={14} /> Generate</>}
                 </button>
               </>
-            ) : (
-              /* ── Key Creation Form ── */
-              <>
-                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-                  <h3 style={{ margin: 0, fontSize: 16 }}>Generate API Key</h3>
-                  <button className="btn btn-ghost btn-sm" onClick={closeGenerateModal}><X size={16} /></button>
-                </div>
+            )
+          }
+        >
+          {generatedKey ? (
+            /* ── Key Created — One-time display ── */
+            <>
+              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
+                <CheckCircle2 size={20} style={{ color: 'var(--color-success)' }} />
+                <span style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>Your key has been created.</span>
+              </div>
 
-                <label style={{ display: 'block', marginBottom: 12 }}>
-                  <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>Name</span>
-                  <input
-                    className="input"
-                    value={newKeyName}
-                    onChange={(e) => setNewKeyName(e.target.value)}
-                    placeholder="e.g. Webhook Integration"
-                    style={{ width: '100%' }}
-                  />
-                </label>
-
-                <label style={{ display: 'block', marginBottom: 12 }}>
-                  <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>Scopes</span>
-                  <div style={{ display: 'flex', gap: 8 }}>
-                    {['read', 'write', 'admin'].map((scope) => (
-                      <button
-                        key={scope}
-                        className={`btn btn-sm ${newKeyScopes.includes(scope) ? 'btn-primary' : 'btn-ghost'}`}
-                        onClick={() => toggleScope(scope)}
-                        style={{ textTransform: 'capitalize' }}
-                      >
-                        {scope}
-                      </button>
-                    ))}
-                  </div>
-                </label>
-
-                <label style={{ display: 'block', marginBottom: 16 }}>
-                  <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>
-                    Expires in (days) — leave empty for no expiry
-                  </span>
-                  <input
-                    className="input"
-                    type="number"
-                    value={newKeyExpiry}
-                    onChange={(e) => setNewKeyExpiry(e.target.value)}
-                    placeholder="90"
-                    min="1"
-                    style={{ width: 120 }}
-                  />
-                </label>
-
-                <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
-                  <button className="btn btn-ghost" onClick={closeGenerateModal}>Cancel</button>
-                  <button
-                    className="btn btn-primary"
-                    onClick={handleGenerateKey}
-                    disabled={generating || !newKeyName.trim()}
-                  >
-                    {generating ? <><RefreshCw size={14} className="spinner" /> Generating...</> : <><Key size={14} /> Generate</>}
-                  </button>
-                </div>
-              </>
-            )}
-          </div>
-        </div>
-      )}
+              <div style={{
+                background: 'var(--color-surface-el)', borderRadius: 'var(--radius-md)',
+                padding: 12, marginBottom: 12, position: 'relative',
+              }}>
+                <code style={{ fontSize: 12, wordBreak: 'break-all', lineHeight: 1.5 }}>
+                  {generatedKey}
+                </code>
+                <button
+                  className="btn btn-ghost btn-sm"
+                  onClick={copyKey}
+                  style={{ position: 'absolute', top: 8, right: 8 }}
+                >
+                  {keyCopied ? <Check size={14} style={{ color: 'var(--color-success)' }} /> : <Copy size={14} />}
+                </button>
+              </div>
 
-      {/* ── Connect Platform Modal ────────────────────────────────────────── */}
-      {connectPlatform && (
-        <div style={{
-          position: 'fixed', inset: 0, zIndex: 1000,
-          background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center',
-        }}>
-          <div className="card" style={{ padding: 24, width: 440, maxWidth: '90vw' }}>
-            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
-              <h3 style={{ margin: 0, fontSize: 16, textTransform: 'capitalize' }}>
-                Connect {connectPlatform}
-              </h3>
-              <button className="btn btn-ghost btn-sm" onClick={() => setConnectPlatform(null)}><X size={16} /></button>
-            </div>
+              <div style={{
+                display: 'flex', alignItems: 'center', gap: 6, padding: 10,
+                background: 'rgba(244, 63, 94, 0.1)', borderRadius: 'var(--radius-sm)',
+                fontSize: 12, color: 'var(--color-error)',
+              }}>
+                <AlertTriangle size={14} />
+                This key will not be shown again. Copy it now.
+              </div>
+            </>
+          ) : (
+            /* ── Key Creation Form ── */
+            <>
+              <label style={{ display: 'block', marginBottom: 12 }}>
+                <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>Name</span>
+                <input
+                  className="input"
+                  value={newKeyName}
+                  onChange={(e) => setNewKeyName(e.target.value)}
+                  placeholder="e.g. Webhook Integration"
+                  style={{ width: '100%' }}
+                />
+              </label>
 
-            {(PLATFORM_FIELDS[connectPlatform] || []).map((field) => (
-              <label key={field.key} style={{ display: 'block', marginBottom: 12 }}>
-                <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>
-                  {field.label}
-                </span>
-                <div style={{ position: 'relative' }}>
-                  <input
-                    className="input"
-                    type={field.secret && !fieldVisibility[field.key] ? 'password' : 'text'}
-                    value={connectFields[field.key] || ''}
-                    onChange={(e) => setConnectFields((prev) => ({ ...prev, [field.key]: e.target.value }))}
-                    placeholder={field.secret ? '••••••••' : field.label}
-                    style={{ width: '100%', paddingRight: field.secret ? 36 : undefined }}
-                  />
-                  {field.secret && (
+              <label style={{ display: 'block', marginBottom: 12 }}>
+                <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>Scopes</span>
+                <div style={{ display: 'flex', gap: 8 }}>
+                  {['read', 'write', 'admin'].map((scope) => (
                     <button
-                      className="btn btn-ghost btn-sm"
-                      style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
-                      onClick={() => setFieldVisibility((prev) => ({ ...prev, [field.key]: !prev[field.key] }))}
+                      key={scope}
+                      className={`btn btn-sm ${newKeyScopes.includes(scope) ? 'btn-primary' : 'btn-ghost'}`}
+                      onClick={() => toggleScope(scope)}
+                      style={{ textTransform: 'capitalize' }}
                     >
-                      {fieldVisibility[field.key] ? <EyeOff size={14} /> : <Eye size={14} />}
+                      {scope}
                     </button>
-                  )}
+                  ))}
                 </div>
               </label>
-            ))}
 
-            <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
+              <label style={{ display: 'block' }}>
+                <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>
+                  Expires in (days) — leave empty for no expiry
+                </span>
+                <input
+                  className="input"
+                  type="number"
+                  value={newKeyExpiry}
+                  onChange={(e) => setNewKeyExpiry(e.target.value)}
+                  placeholder="90"
+                  min="1"
+                  style={{ width: 120 }}
+                />
+              </label>
+            </>
+          )}
+        </ResizableModal>
+      )}
+
+      {/* ── Connect Platform Modal ────────────────────────────────────────── */}
+      {connectPlatform && (
+        <ResizableModal
+          modalId="api-platform-connect"
+          title={<span style={{ textTransform: 'capitalize' }}>Connect {connectPlatform}</span>}
+          onClose={() => setConnectPlatform(null)}
+          defaultWidth={520}
+          footer={
+            <>
               <button className="btn btn-ghost" onClick={() => setConnectPlatform(null)}>Cancel</button>
               <button
                 className="btn btn-primary"
@@ -696,55 +662,86 @@ export default function ApiCredentialsSection() {
               >
                 {connecting ? <><RefreshCw size={14} className="spinner" /> Connecting...</> : <><Plug size={14} /> Connect</>}
               </button>
-            </div>
-          </div>
-        </div>
+            </>
+          }
+        >
+          {(PLATFORM_FIELDS[connectPlatform] || []).map((field) => (
+            <label key={field.key} style={{ display: 'block', marginBottom: 12 }}>
+              <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 4, display: 'block' }}>
+                {field.label}
+              </span>
+              <div style={{ position: 'relative' }}>
+                <input
+                  className="input"
+                  type={field.secret && !fieldVisibility[field.key] ? 'password' : 'text'}
+                  value={connectFields[field.key] || ''}
+                  onChange={(e) => setConnectFields((prev) => ({ ...prev, [field.key]: e.target.value }))}
+                  placeholder={field.secret ? '••••••••' : field.label}
+                  style={{ width: '100%', paddingRight: field.secret ? 36 : undefined }}
+                />
+                {field.secret && (
+                  <button
+                    className="btn btn-ghost btn-sm"
+                    style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
+                    onClick={() => setFieldVisibility((prev) => ({ ...prev, [field.key]: !prev[field.key] }))}
+                  >
+                    {fieldVisibility[field.key] ? <EyeOff size={14} /> : <Eye size={14} />}
+                  </button>
+                )}
+              </div>
+            </label>
+          ))}
+        </ResizableModal>
       )}
 
       {/* ── Revoke Key Confirm ────────────────────────────────────────────── */}
       {revokeId && (
-        <div style={{
-          position: 'fixed', inset: 0, zIndex: 1000,
-          background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center',
-        }}>
-          <div className="card" style={{ padding: 24, width: 380, textAlign: 'center' }}>
-            <AlertTriangle size={32} style={{ color: 'var(--color-error)', marginBottom: 12 }} />
-            <h3 style={{ margin: '0 0 8px', fontSize: 16 }}>Revoke API Key?</h3>
-            <p style={{ color: 'var(--color-text-secondary)', fontSize: 13, marginBottom: 16 }}>
-              This key will immediately stop working. This action cannot be undone.
-            </p>
-            <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
+        <ResizableModal
+          modalId="api-key-revoke-confirm"
+          title="Revoke API Key?"
+          onClose={() => setRevokeId(null)}
+          defaultWidth={400}
+          footer={
+            <>
               <button className="btn btn-ghost" onClick={() => setRevokeId(null)}>Cancel</button>
               <button className="btn btn-danger" onClick={handleRevokeKey} disabled={revoking}>
                 {revoking ? 'Revoking...' : 'Revoke Key'}
               </button>
-            </div>
+            </>
+          }
+        >
+          <div style={{ textAlign: 'center' }}>
+            <AlertTriangle size={32} style={{ color: 'var(--color-error)', marginBottom: 12 }} />
+            <p style={{ color: 'var(--color-text-secondary)', fontSize: 13, margin: 0 }}>
+              This key will immediately stop working. This action cannot be undone.
+            </p>
           </div>
-        </div>
+        </ResizableModal>
       )}
 
       {/* ── Disconnect Confirm ────────────────────────────────────────────── */}
       {disconnectPlatform && (
-        <div style={{
-          position: 'fixed', inset: 0, zIndex: 1000,
-          background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center',
-        }}>
-          <div className="card" style={{ padding: 24, width: 380, textAlign: 'center' }}>
-            <Unplug size={32} style={{ color: 'var(--color-warning)', marginBottom: 12 }} />
-            <h3 style={{ margin: '0 0 8px', fontSize: 16, textTransform: 'capitalize' }}>
-              Disconnect {disconnectPlatform}?
-            </h3>
-            <p style={{ color: 'var(--color-text-secondary)', fontSize: 13, marginBottom: 16 }}>
-              All stored credentials will be cleared. The agent will stop working until reconnected.
-            </p>
-            <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
+        <ResizableModal
+          modalId="api-platform-disconnect-confirm"
+          title={<span style={{ textTransform: 'capitalize' }}>Disconnect {disconnectPlatform}?</span>}
+          onClose={() => setDisconnectPlatform(null)}
+          defaultWidth={400}
+          footer={
+            <>
               <button className="btn btn-ghost" onClick={() => setDisconnectPlatform(null)}>Cancel</button>
               <button className="btn btn-danger" onClick={handleDisconnect} disabled={disconnecting}>
                 {disconnecting ? 'Disconnecting...' : 'Disconnect'}
               </button>
-            </div>
+            </>
+          }
+        >
+          <div style={{ textAlign: 'center' }}>
+            <Unplug size={32} style={{ color: 'var(--color-warning)', marginBottom: 12 }} />
+            <p style={{ color: 'var(--color-text-secondary)', fontSize: 13, margin: 0 }}>
+              All stored credentials will be cleared. The agent will stop working until reconnected.
+            </p>
           </div>
-        </div>
+        </ResizableModal>
       )}
     </div>
   );
diff --git a/components/social/SocialTab.tsx b/components/social/SocialTab.tsx
index fdae3da..119930d 100644
--- a/components/social/SocialTab.tsx
+++ b/components/social/SocialTab.tsx
@@ -13,6 +13,7 @@ import {
   History,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
+import ResizableModal from '../shared/ResizableModal';
 import AnalyticsDashboard from './phase2/AnalyticsDashboard';
 import ApprovalQueue from './phase2/ApprovalQueue';
 import AIImageGenerator from './phase2/AIImageGenerator';
@@ -947,57 +948,38 @@ function AddStreamModal({
   onClose: () => void;
 }) {
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 60,
-        background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(4px)',
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-      }}
-      onClick={(e) => e.target === e.currentTarget && onClose()}
+    <ResizableModal
+      modalId="social-add-stream"
+      title="Add Stream"
+      onClose={onClose}
+      defaultWidth={560}
     >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        exit={{ opacity: 0, scale: 0.95 }}
-        transition={{ duration: 0.15 }}
-        style={{
-          background: 'var(--color-surface)', borderRadius: 'var(--radius-lg)',
-          border: '1px solid var(--color-border)', padding: 24, width: 560, maxWidth: '95vw',
-        }}
-      >
-        <div className="flex items-center justify-between" style={{ marginBottom: 18 }}>
-          <h3 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)' }}>Add Stream</h3>
-          <button className="btn btn-ghost btn-sm" onClick={onClose} style={{ minHeight: 'unset', minWidth: 'unset', padding: 6 }}>
-            <X size={16} />
+      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
+        {(Object.entries(STREAM_TYPE_META) as [StreamType, typeof STREAM_TYPE_META[StreamType]][]).map(([type, meta]) => (
+          <button
+            key={type}
+            onClick={() => { onAdd(type); onClose(); }}
+            style={{
+              textAlign: 'left', padding: 14, background: 'var(--color-bg)',
+              border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)',
+              cursor: 'pointer', transition: 'border-color 0.15s ease, background 0.15s ease',
+            }}
+            onMouseEnter={(e) => {
+              (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--color-primary)';
+              (e.currentTarget as HTMLButtonElement).style.background = 'rgba(10,124,89,0.04)';
+            }}
+            onMouseLeave={(e) => {
+              (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--color-border)';
+              (e.currentTarget as HTMLButtonElement).style.background = 'var(--color-bg)';
+            }}
+          >
+            <meta.Icon size={18} style={{ color: 'var(--color-primary)', marginBottom: 6 }} />
+            <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--color-text)', marginBottom: 3 }}>{meta.label}</div>
+            <div style={{ fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>{meta.desc}</div>
           </button>
-        </div>
-        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
-          {(Object.entries(STREAM_TYPE_META) as [StreamType, typeof STREAM_TYPE_META[StreamType]][]).map(([type, meta]) => (
-            <button
-              key={type}
-              onClick={() => { onAdd(type); onClose(); }}
-              style={{
-                textAlign: 'left', padding: 14, background: 'var(--color-bg)',
-                border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)',
-                cursor: 'pointer', transition: 'border-color 0.15s ease, background 0.15s ease',
-              }}
-              onMouseEnter={(e) => {
-                (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--color-primary)';
-                (e.currentTarget as HTMLButtonElement).style.background = 'rgba(10,124,89,0.04)';
-              }}
-              onMouseLeave={(e) => {
-                (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--color-border)';
-                (e.currentTarget as HTMLButtonElement).style.background = 'var(--color-bg)';
-              }}
-            >
-              <meta.Icon size={18} style={{ color: 'var(--color-primary)', marginBottom: 6 }} />
-              <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--color-text)', marginBottom: 3 }}>{meta.label}</div>
-              <div style={{ fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.4 }}>{meta.desc}</div>
-            </button>
-          ))}
-        </div>
-      </motion.div>
-    </div>
+        ))}
+      </div>
+    </ResizableModal>
   );
 }
 
@@ -1296,52 +1278,16 @@ function ComposeModal({ onClose, prefill = '', editPost, onSaved }: ComposeModal
   }
 
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 70,
-        background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(6px)',
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-        padding: 20,
-      }}
-      onClick={(e) => e.target === e.currentTarget && onClose()}
-    >
-      <motion.div
-        initial={{ opacity: 0, y: 20, scale: 0.97 }}
-        animate={{ opacity: 1, y: 0, scale: 1 }}
-        exit={{ opacity: 0, y: 20, scale: 0.97 }}
-        transition={{ duration: 0.18 }}
-        style={{
-          width: '100%', maxWidth: 1000,
-          background: 'var(--color-surface)',
-          borderRadius: 'var(--radius-lg)',
-          border: '1px solid var(--color-border)',
-          boxShadow: '0 20px 60px rgba(0,0,0,0.18)',
-          display: 'flex',
-          flexDirection: 'column',
-          maxHeight: '90vh',
-          overflow: 'hidden',
-        }}
+    <>
+      <ResizableModal
+        modalId="social-compose-post"
+        title={editPost ? 'Edit Post' : 'New Post'}
+        onClose={onClose}
+        defaultWidth={1000}
+        defaultHeight={720}
       >
-        {/* Modal header */}
-        <div className="flex items-center justify-between" style={{
-          padding: '14px 20px',
-          borderBottom: '1px solid var(--color-border)',
-          flexShrink: 0,
-        }}>
-          <h2 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
-            {editPost ? 'Edit Post' : 'New Post'}
-          </h2>
-          <button
-            className="btn btn-ghost btn-sm"
-            onClick={onClose}
-            style={{ minHeight: 'unset', minWidth: 'unset', padding: 6 }}
-          >
-            <X size={18} />
-          </button>
-        </div>
-
-        {/* Two-panel body */}
-        <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
+        {/* Negate ResizableModal body padding so split-panel + dividers extend edge-to-edge */}
+        <div style={{ margin: '-16px -20px', display: 'flex', height: '100%', overflow: 'hidden' }}>
 
           {/* LEFT PANEL — 55% */}
           <div style={{
@@ -1684,7 +1630,7 @@ function ComposeModal({ onClose, prefill = '', editPost, onSaved }: ComposeModal
             </div>
           </div>
         </div>
-      </motion.div>
+      </ResizableModal>
 
       {/* Nested AI Image Generator modal */}
       {showAIImage && (
@@ -1693,7 +1639,7 @@ function ComposeModal({ onClose, prefill = '', editPost, onSaved }: ComposeModal
           onClose={() => setShowAIImage(false)}
         />
       )}
-    </div>
+    </>
   );
 }
 
diff --git a/components/social/phase2/BulkScheduleModal.tsx b/components/social/phase2/BulkScheduleModal.tsx
index 2c59f12..d22edd3 100644
--- a/components/social/phase2/BulkScheduleModal.tsx
+++ b/components/social/phase2/BulkScheduleModal.tsx
@@ -1,13 +1,13 @@
 'use client';
 
-import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
+import { useState, useEffect, useMemo, useRef } from 'react';
 import {
-  X, Upload, FileText, Calendar, Clock, Check, ChevronDown,
+  Upload, FileText, Calendar, Clock, Check,
   Twitter, Instagram, Linkedin, Facebook, Globe, AtSign, Video,
   AlertCircle, Sparkles,
 } from 'lucide-react';
 import { useToast } from '../../ToastProvider';
+import ResizableModal from '../../shared/ResizableModal';
 
 /* ─────────────────────────────────────────────────────────────────────────── */
 /* TYPES & CONSTANTS                                                            */
@@ -317,69 +317,80 @@ export default function BulkScheduleModal({ open, onClose, onScheduled }: BulkSc
     return () => window.removeEventListener('keydown', onKey);
   }, [open, onClose]);
 
+  if (!open) return null;
+
   return (
-    <AnimatePresence>
-      {open && (
-        <div
-          style={{
-            position: 'fixed', inset: 0, zIndex: 80,
-            background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(6px)',
-            display: 'flex', alignItems: 'center', justifyContent: 'center',
-            padding: 20,
-          }}
-          onClick={(e) => e.target === e.currentTarget && onClose()}
-        >
-          <motion.div
-            initial={{ opacity: 0, y: 20, scale: 0.97 }}
-            animate={{ opacity: 1, y: 0, scale: 1 }}
-            exit={{ opacity: 0, y: 20, scale: 0.97 }}
-            transition={{ duration: 0.18 }}
+    <ResizableModal
+      modalId="social-bulk-schedule"
+      title={
+        <span className="flex items-center gap-2">
+          <Sparkles size={15} style={{ color: 'var(--color-primary)' }} />
+          <span>Bulk Schedule Posts</span>
+          {parsedPosts.length > 0 && (
+            <span style={{
+              padding: '2px 9px', borderRadius: 99, fontSize: 11, fontWeight: 700,
+              background: 'rgba(16,185,129,0.15)', color: 'var(--color-primary)',
+              border: '1px solid rgba(16,185,129,0.3)',
+            }}>
+              {parsedPosts.length} posts detected
+            </span>
+          )}
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={980}
+      footer={
+        <>
+          <div style={{ flex: 1 }}>
+            {error && (
+              <div className="flex items-center gap-2" style={{ fontSize: 12, color: 'var(--color-error)' }}>
+                <AlertCircle size={13} />
+                {error}
+              </div>
+            )}
+          </div>
+          <button
+            onClick={onClose}
+            className="btn btn-ghost btn-sm"
+            disabled={submitting}
+          >
+            Cancel
+          </button>
+          <button
+            onClick={handleSubmit}
+            disabled={submitting || parsedPosts.length === 0 || selectedPlatforms.length === 0}
+            className="btn btn-primary"
             style={{
-              width: '100%', maxWidth: 980,
-              background: 'var(--color-surface)',
-              borderRadius: 'var(--radius-lg)',
-              border: '1px solid var(--color-border)',
-              boxShadow: '0 24px 64px rgba(0,0,0,0.22)',
-              display: 'flex', flexDirection: 'column',
-              maxHeight: '92vh', overflow: 'hidden',
+              display: 'inline-flex', alignItems: 'center', gap: 6,
+              opacity: (submitting || parsedPosts.length === 0 || selectedPlatforms.length === 0) ? 0.6 : 1,
             }}
-            role="dialog"
-            aria-modal="true"
-            aria-label="Bulk Schedule Modal"
           >
-            {/* Modal header */}
-            <div className="flex items-center justify-between" style={{
-              padding: '14px 20px',
-              borderBottom: '1px solid var(--color-border)',
-              flexShrink: 0,
-            }}>
-              <div className="flex items-center gap-2">
-                <Sparkles size={16} style={{ color: 'var(--color-primary)' }} />
-                <h2 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
-                  Bulk Schedule Posts
-                </h2>
-                {parsedPosts.length > 0 && (
-                  <span style={{
-                    padding: '2px 9px', borderRadius: 99, fontSize: 11, fontWeight: 700,
-                    background: 'rgba(16,185,129,0.15)', color: 'var(--color-primary)',
-                    border: '1px solid rgba(16,185,129,0.3)',
-                  }}>
-                    {parsedPosts.length} posts detected
-                  </span>
-                )}
-              </div>
-              <button
-                className="btn btn-ghost btn-sm"
-                onClick={onClose}
-                style={{ padding: 6, minHeight: 'unset', minWidth: 'unset' }}
-                aria-label="Close bulk schedule modal"
-              >
-                <X size={18} />
-              </button>
-            </div>
-
+            {submitting ? (
+              <>
+                <div className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} />
+                Scheduling…
+              </>
+            ) : (
+              <>
+                <Calendar size={14} />
+                Schedule {parsedPosts.length > 0 ? `${parsedPosts.length} ` : ''}post{parsedPosts.length !== 1 ? 's' : ''}
+              </>
+            )}
+          </button>
+        </>
+      }
+    >
             {/* Two-panel body */}
-            <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
+            <div style={{
+              display: 'flex',
+              flex: 1,
+              overflow: 'hidden',
+              marginLeft: -20,
+              marginRight: -20,
+              marginTop: -16,
+              marginBottom: -16,
+              minHeight: 480,
+            }}>
 
               {/* ── LEFT PANEL: Content input ─────────────────────────────── */}
               <div style={{
@@ -661,58 +672,6 @@ export default function BulkScheduleModal({ open, onClose, onScheduled }: BulkSc
               </div>
             </div>
 
-            {/* Footer */}
-            <div style={{
-              padding: '12px 20px',
-              borderTop: '1px solid var(--color-border)',
-              display: 'flex', alignItems: 'center', justifyContent: 'space-between',
-              flexShrink: 0,
-              background: 'var(--color-surface-el)',
-              flexWrap: 'wrap',
-              gap: 8,
-            }}>
-              <div style={{ flex: 1 }}>
-                {error && (
-                  <div className="flex items-center gap-2" style={{ fontSize: 12, color: 'var(--color-error)' }}>
-                    <AlertCircle size={13} />
-                    {error}
-                  </div>
-                )}
-              </div>
-              <div className="flex items-center gap-2">
-                <button
-                  onClick={onClose}
-                  className="btn btn-ghost btn-sm"
-                  disabled={submitting}
-                >
-                  Cancel
-                </button>
-                <button
-                  onClick={handleSubmit}
-                  disabled={submitting || parsedPosts.length === 0 || selectedPlatforms.length === 0}
-                  className="btn btn-primary"
-                  style={{
-                    display: 'inline-flex', alignItems: 'center', gap: 6,
-                    opacity: (submitting || parsedPosts.length === 0 || selectedPlatforms.length === 0) ? 0.6 : 1,
-                  }}
-                >
-                  {submitting ? (
-                    <>
-                      <div className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} />
-                      Scheduling…
-                    </>
-                  ) : (
-                    <>
-                      <Calendar size={14} />
-                      Schedule {parsedPosts.length > 0 ? `${parsedPosts.length} ` : ''}post{parsedPosts.length !== 1 ? 's' : ''}
-                    </>
-                  )}
-                </button>
-              </div>
-            </div>
-          </motion.div>
-        </div>
-      )}
-    </AnimatePresence>
+    </ResizableModal>
   );
 }
diff --git a/components/statements/StatementsTab.tsx b/components/statements/StatementsTab.tsx
index 1faf658..01557ba 100644
--- a/components/statements/StatementsTab.tsx
+++ b/components/statements/StatementsTab.tsx
@@ -11,6 +11,7 @@ import {
 } from 'lucide-react';
 import { useToast } from '@/components/ToastProvider';
 import { getOutletFaviconUrl } from '@/lib/outlet-logo';
+import ResizableModal from '@/components/shared/ResizableModal';
 
 /* ─── Types ──────────────────────────────────────────────────────────────── */
 
@@ -209,36 +210,41 @@ function GenerateModal({
   }
 
   return (
-    <div
-      style={{
-        position: 'fixed', inset: 0, zIndex: 100,
-        display: 'flex', alignItems: 'center', justifyContent: 'center',
-        background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(4px)',
-      }}
-      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
-    >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        exit={{ opacity: 0, scale: 0.95 }}
-        style={{
-          background: 'var(--color-surface)',
-          border: '1px solid var(--color-border)',
-          borderRadius: 'var(--radius-lg)',
-          width: '100%', maxWidth: 620, maxHeight: '90vh',
-          overflow: 'auto', padding: 24,
-        }}
-      >
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
-          <h2 style={{ fontSize: 18, fontWeight: 600, color: 'var(--color-text)', display: 'flex', alignItems: 'center', gap: 8 }}>
-            <Sparkles size={20} color="var(--color-secondary)" />
-            Generate AI Statement
-          </h2>
-          <button onClick={onClose} className="btn btn-ghost" style={{ padding: 4 }}>
-            <X size={18} />
+    <ResizableModal
+      modalId="statements-generate"
+      title={
+        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <Sparkles size={20} color="var(--color-secondary)" />
+          Generate AI Statement
+        </span>
+      }
+      onClose={onClose}
+      defaultWidth={620}
+      footer={
+        <>
+          <button className="btn btn-secondary" onClick={onClose}>Cancel</button>
+          <button
+            className="btn btn-primary"
+            onClick={handleGenerate}
+            disabled={generating || !eventTitle.trim()}
+            style={{ display: 'flex', alignItems: 'center', gap: 6 }}
+          >
+            {generating ? (
+              <>
+                <span className="spinner" style={{ width: 14, height: 14 }} />
+                Generating...
+              </>
+            ) : (
+              <>
+                <Sparkles size={14} />
+                Generate Statement
+              </>
+            )}
           </button>
-        </div>
-
+        </>
+      }
+    >
+      <>
         {newsItems.length > 0 && (
           <div style={{ marginBottom: 16 }}>
             <label style={{ fontSize: 13, color: 'var(--color-text-secondary)', marginBottom: 6, display: 'block' }}>
@@ -345,29 +351,8 @@ function GenerateModal({
           </div>
         </div>
 
-        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 20 }}>
-          <button className="btn btn-secondary" onClick={onClose}>Cancel</button>
-          <button
-            className="btn btn-primary"
-            onClick={handleGenerate}
-            disabled={generating || !eventTitle.trim()}
-            style={{ display: 'flex', alignItems: 'center', gap: 6 }}
-          >
-            {generating ? (
-              <>
-                <span className="spinner" style={{ width: 14, height: 14 }} />
-                Generating...
-              </>
-            ) : (
-              <>
-                <Sparkles size={14} />
-                Generate Statement
-              </>
-            )}
-          </button>
-        </div>
-      </motion.div>
-    </div>
+      </>
+    </ResizableModal>
   );
 }
 
diff --git a/components/xsessions/XSessionsTab.tsx b/components/xsessions/XSessionsTab.tsx
index 2f6e491..efa4ce7 100644
--- a/components/xsessions/XSessionsTab.tsx
+++ b/components/xsessions/XSessionsTab.tsx
@@ -3,13 +3,14 @@
 import { useState, useEffect, useCallback } from 'react';
 import { motion, AnimatePresence } from 'framer-motion';
 import {
-  Search, Plus, Trash2, Check, X, RefreshCw, Clock,
+  Search, Plus, Trash2, Check, RefreshCw, Clock,
   Heart, Repeat, MessageCircle, Eye, ChevronDown, ChevronUp,
   Hash, Twitter, Image, Filter,
 } from 'lucide-react';
 import { useToast } from '../ToastProvider';
 import SortDropdown from '../shared/SortDropdown';
 import { useClientSort, SortConfig } from '@/hooks/useClientSort';
+import ResizableModal from '../shared/ResizableModal';
 
 const XSESS_SORT_OPTIONS = [
   { value: 'date_desc', label: 'Newest First' },
@@ -96,12 +97,6 @@ function NewScanModal({ onClose, onCreated }: { onClose: () => void; onCreated:
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState('');
 
-  useEffect(() => {
-    const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
-    window.addEventListener('keydown', handleKey);
-    return () => window.removeEventListener('keydown', handleKey);
-  }, [onClose]);
-
   const presets = [
     'policy advocacy',
     'nonprofit reform',
@@ -140,87 +135,80 @@ function NewScanModal({ onClose, onCreated }: { onClose: () => void; onCreated:
   }
 
   return (
-    <div
-      className="fixed inset-0 z-50 flex items-center justify-center"
-      style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
-      onClick={onClose}
-    >
-      <motion.div
-        initial={{ opacity: 0, scale: 0.95 }}
-        animate={{ opacity: 1, scale: 1 }}
-        className="w-full max-w-lg mx-4 rounded-xl p-6"
-        style={{ backgroundColor: 'var(--color-surface)', border: '1px solid var(--color-border)' }}
-        onClick={(e) => e.stopPropagation()}
-      >
-        <div className="flex items-center justify-between mb-5">
-          <h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
-            New X Scan
-          </h2>
-          <button onClick={onClose} className="btn btn-ghost btn-sm">
-            <X size={16} />
+    <ResizableModal
+      modalId="xsessions-new-scan"
+      title="New X Scan"
+      onClose={onClose}
+      defaultWidth={520}
+      footer={
+        <>
+          <button type="button" className="btn btn-ghost" onClick={onClose}>
+            Cancel
           </button>
+          <button
+            type="button"
+            className="btn btn-primary"
+            disabled={loading || !terms.trim()}
+            onClick={(e) => handleSubmit(e as unknown as React.FormEvent)}
+          >
+            {loading ? (
+              <>
+                <RefreshCw size={14} className="animate-spin" />
+                <span>Scanning...</span>
+              </>
+            ) : (
+              <>
+                <Search size={14} />
+                <span>Start Scan</span>
+              </>
+            )}
+          </button>
+        </>
+      }
+    >
+      <form onSubmit={handleSubmit}>
+        <label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-secondary)' }}>
+          Search Terms
+        </label>
+        <input
+          type="text"
+          className="input w-full mb-3"
+          value={terms}
+          onChange={(e) => setTerms(e.target.value)}
+          placeholder="e.g. policy advocacy"
+          aria-label="Search terms"
+          autoFocus
+        />
+
+        <label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-secondary)' }}>
+          Quick Presets
+        </label>
+        <div className="flex flex-wrap gap-2 mb-4">
+          {presets.map((p) => (
+            <button
+              key={p}
+              type="button"
+              className="text-xs px-2 py-1 rounded-full transition-colors"
+              style={{
+                backgroundColor: terms === p ? 'var(--color-primary)' : 'var(--color-surface-el)',
+                color: terms === p ? '#fff' : 'var(--color-text-secondary)',
+                border: '1px solid var(--color-border)',
+              }}
+              onClick={() => setTerms(p)}
+            >
+              {p}
+            </button>
+          ))}
         </div>
 
-        <form onSubmit={handleSubmit}>
-          <label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-secondary)' }}>
-            Search Terms
-          </label>
-          <input
-            type="text"
-            className="input w-full mb-3"
-            value={terms}
-            onChange={(e) => setTerms(e.target.value)}
-            placeholder="e.g. policy advocacy"
-            aria-label="Search terms"
-            autoFocus
-          />
-
-          <label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-secondary)' }}>
-            Quick Presets
-          </label>
-          <div className="flex flex-wrap gap-2 mb-4">
-            {presets.map((p) => (
-              <button
-                key={p}
-                type="button"
-                className="text-xs px-2 py-1 rounded-full transition-colors"
-                style={{
-                  backgroundColor: terms === p ? 'var(--color-primary)' : 'var(--color-surface-el)',
-                  color: terms === p ? '#fff' : 'var(--color-text-secondary)',
-                  border: '1px solid var(--color-border)',
-                }}
-                onClick={() => setTerms(p)}
-              >
-                {p}
-              </button>
-            ))}
-          </div>
-
-          {error && (
-            <p className="text-sm mb-3" style={{ color: '#f87171' }}>{error}</p>
-          )}
+        {error && (
+          <p className="text-sm mb-3" style={{ color: '#f87171' }}>{error}</p>
+        )}
 
-          <div className="flex justify-end gap-2 mt-4">
-            <button type="button" className="btn btn-ghost" onClick={onClose}>
-              Cancel
-            </button>
-            <button type="submit" className="btn btn-primary" disabled={loading || !terms.trim()}>
-              {loading ? (
-                <>
-                  <RefreshCw size={14} className="animate-spin" />
-                  <span>Scanning...</span>
-                </>
-              ) : (
-                <>
-                  <Search size={14} />
-                  <span>Start Scan</span>
-                </>
-              )}
-            </button>
-          </div>
-        </form>
-      </motion.div>
-    </div>
+        {/* Hidden submit to enable Enter-key submission */}
+        <button type="submit" style={{ display: 'none' }} aria-hidden="true" tabIndex={-1} />
+      </form>
+    </ResizableModal>
   );
 }
 

← dfa8907 feat(inbox): +16 themes (v17-v32) — total 32 live + dropdown  ·  back to Norma  ·  fix(gmail): scope Gmail CRM to current user's mailbox, not h 8f71b14 →