PageSourceSearch

https://rrccdashboard.netlify.app/board-members

html rrccdashboard.netlify.app collected 2026-10-03 11:21:25 UTC 100,460 bytes, 1,428 lines download raw bytes

1<!doctype html>
2<html lang="en">
3<head>
4  <meta charset="utf-8" />
5  <meta name="viewport" content="width=device-width, initial-scale=1" />
6  <link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
7  <title>RRCC — Board Members</title>
8  <link rel="stylesheet" href="styles.css?v=5b11c888b4" />
9  <style>
10    .progtabs{display:flex;gap:6px;max-width:1320px;margin:12px auto 0;padding:0 14px;flex-wrap:wrap}
11    .progtab{display:inline-flex;align-items:center;text-decoration:none;font-weight:800;font-size:14.5px;color:#1f2a44;background:#fff;border:1px solid #e1e6ef;border-radius:12px 12px 0 0;padding:11px 22px;box-shadow:0 1px 4px rgba(20,20,40,.06);position:relative;top:1px}
12    .progtab:hover{color:#b8232a;border-color:#f0b8b8}
13    .progtab.active{background:#b8232a;border-color:#b8232a;color:#fff}
14    .topnav{display:flex;align-items:center;gap:18px;background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:10px 16px;margin-bottom:14px;box-shadow:0 1px 4px rgba(20,20,40,.06);flex-wrap:wrap;position:sticky;top:0;z-index:200}
15    #featBtn{flex:none;white-space:nowrap;cursor:pointer}
16    .navlink{display:inline-flex;align-items:center;text-decoration:none;font-weight:700;font-size:13px;color:#1f2a44;background:#f3f5f9;border:1px solid #e1e6ef;border-radius:10px;padding:8px 11px}
17    .navlink:hover{background:#fdecec;border-color:#f0b8b8;color:#b8232a}
18
19    .wrap{max-width:1140px;margin:0 auto;padding:0 22px 30px}
20    .head h1{margin:0 0 3px;font-size:22px;color:#1f2a44}
21    .head p{margin:0 0 10px;color:#6b7280;font-size:13px;max-width:860px}
22    .countbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
23    .chip{font-size:12px;font-weight:700;color:#475569;background:#eef1f5;border:1px solid #e1e6ef;border-radius:999px;padding:4px 11px}
24    .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
25    .toolbar input[type=search]{font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:9px;background:#fff;min-width:220px;flex:1}
26    .toolbar label{font-size:12.5px;color:#475569;display:inline-flex;align-items:center;gap:5px;font-weight:600}
27    .actionbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px;background:#fff;border:1px solid #e6e8ec;border-radius:12px;padding:9px 12px}
28    .actionbar .sel{font-size:13px;font-weight:700;color:#1f2a44}
29    .btn{font:inherit;font-weight:700;font-size:12.5px;padding:7px 12px;border-radius:9px;border:1px solid #d7dbe0;background:#fff;color:#1f2a44;cursor:pointer;white-space:nowrap}
30    .btn:hover{border-color:#b8232a;color:#b8232a}
31    .btn.primary{background:#1f2a44;border-color:#1f2a44;color:#fff}.btn.primary:hover{background:#0f1830;color:#fff}
32    .btn.sm{padding:5px 9px;font-size:12px}
33    .btn[disabled]{opacity:.5;cursor:default;border-color:#e1e6ef;color:#9aa1ab}
34
35    .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(330px,1fr));gap:12px}
36    .mcard{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:14px;box-shadow:0 1px 3px rgba(20,20,40,.05);display:flex;flex-direction:column;gap:9px}
37    .mcard.sel{border-color:#b8232a;box-shadow:0 0 0 2px #fdecec}
38    .mtop{display:flex;gap:11px;align-items:flex-start}
39    .ava{width:54px;height:54px;border-radius:50%;flex:none;object-fit:cover;background:#eef1f5;display:flex;align-items:center;justify-content:center;font-weight:800;color:#9aa1ab;font-size:18px;border:1px solid #e1e6ef;overflow:hidden}
40    .ava img{width:100%;height:100%;object-fit:cover}
41    .mname{font-weight:800;color:#1f2a44;font-size:16px;line-height:1.2}
42    .mpos{display:inline-block;font-size:11px;font-weight:800;border-radius:999px;padding:2px 9px;margin-top:3px}
43    .p-chair{color:#7c2d12;background:#fde9d6}.p-vice{color:#92600a;background:#fef3c7}.p-treasurer{color:#065f46;background:#cdeede}.p-secretary{color:#5b21b6;background:#ede9fe}.p-dir{color:#1e40af;background:#dbeafe}
44    .mmeta{font-size:12.5px;color:#475569;line-height:1.5}
45    .mmeta a{color:#b8232a;text-decoration:none}.mmeta a:hover{text-decoration:underline}
46    .mbio{font-size:12.5px;color:#334155;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px;padding:7px 9px;white-space:pre-wrap;overflow-wrap:anywhere}
47    .mbio.clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
48    .mbiomore{display:block;margin:4px 0 0;background:none;border:0;color:#1d4ed8;font-weight:700;font-size:11.5px;cursor:pointer;padding:2px 0}
49    .mterm{font-size:11.5px;color:#9aa1ab}
50    .pick{margin-left:auto;flex:none}
51    .mactions{display:flex;gap:6px;flex-wrap:wrap;border-top:1px solid #f1f3f6;padding-top:9px}
52    .updated{font-size:11px;color:#15803d}
53    .nolink{font-size:11px;color:#92400e;font-weight:600}
54    .msigned{font-size:11px;color:#475569}
55    .msigned.notyet{color:#92400e;font-weight:600}
56    .mrev{font-size:11.5px;color:#1d4ed8;margin-top:6px;font-weight:600}
57    .empty{color:#9aa1ab;text-align:center;padding:40px 0}
58    /* approval rotation panel */
59    .rotation{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(20,20,40,.05)}
60    .rothead{font-weight:800;color:#1f2a44;font-size:14px;margin-bottom:3px}
61    .rotsub{font-size:12.5px;color:#6b7280;margin-bottom:10px}.rotsub b{color:#1d4ed8}
62    .rotlist{display:flex;flex-wrap:wrap;gap:6px}
63    /* board activity log */
64    .actlog{background:#fff;border:1px solid #e6e8ec;border-radius:14px;padding:12px 16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(20,20,40,.05)}
65    .actlog>summary{cursor:pointer;font-weight:800;color:#1f2a44;font-size:14px;list-style:none;display:flex;align-items:center;gap:8px}
66    .actlog>summary::-webkit-details-marker{display:none}
67    .actlog>summary::after{content:"\25BE";color:#9aa1ab;font-size:12px;transition:transform .15s}
68    .actlog[open]>summary::after{transform:rotate(180deg)}
69    .actlist{margin-top:10px;max-height:340px;overflow-y:auto}
70    .actrow{display:flex;gap:12px;align-items:baseline;padding:7px 0;border-bottom:1px solid #f2f4f7;font-size:13px;color:#1f2a44}
71    .actrow:last-child{border-bottom:0}
72    .actwhen{flex:none;width:118px;color:#9aa1ab;font-size:11.5px;font-weight:700;white-space:nowrap}
73    .actempty{color:#9aa1ab;font-size:12.5px;margin:0}
74    .rotrow{display:inline-flex;align-items:center;gap:7px;padding:6px 12px;border:1px solid #eef0f3;border-radius:999px;background:#f7f8fa;cursor:grab;user-select:none;-webkit-user-select:none}
75    .rmove{margin-left:auto;display:flex;gap:4px}
76    .rbtn{border:1px solid #d7dbe3;background:#fff;color:#334155;border-radius:8px;width:34px;height:34px;font:inherit;font-size:14px;line-height:1;cursor:pointer}
77    .rbtn:hover:not(:disabled){border-color:#b8232a;color:#b8232a}
78    .rbtn:disabled{opacity:.35;cursor:default}
79    .rotrow:active{cursor:grabbing}
80    .rotrow.dragging{opacity:.45;border-style:dashed}
81    .rotrow.next{border-color:#1d4ed8;background:#eef3ff}
82    .rotrow .rnum{font-weight:800;color:#9aa1ab;font-size:11px}
83    .rotrow .rname{font-weight:700;color:#1f2a44;font-size:12.5px;white-space:nowrap}
84    .rotrow .rnext{font-size:9px;font-weight:800;text-transform:uppercase;color:#fff;background:#1d4ed8;border-radius:999px;padding:1px 6px;margin-left:5px}
85
86    .ov{position:fixed;inset:0;background:rgba(15,20,35,.5);display:flex;align-items:center;justify-content:center;z-index:500;padding:16px}
87    .ov[hidden]{display:none}
88    .modal{background:#fff;border-radius:16px;padding:20px 22px;max-width:460px;width:100%;box-shadow:0 20px 60px rgba(0,0,0,.3);max-height:90vh;overflow:auto}
89    .modal h3{margin:0 0 12px;color:#1f2a44;font-size:18px}
90    .modal.wide{max-width:560px}
91    .pf-head{display:flex;align-items:center;gap:14px;margin-bottom:12px}
92    .pf-ava{width:60px;height:60px;border-radius:50%;flex:none;background:#eef1f5;border:1px solid #e1e6ef;display:flex;align-items:center;justify-content:center;color:#9aa1ab;font-weight:800;font-size:20px;overflow:hidden}
93    .pf-ava img{width:100%;height:100%;object-fit:cover}
94    .pf-name{font-size:18px;font-weight:800;color:#1f2a44}
95    .modal h3.pf-name{margin:0 0 3px}
96    .pf-pos{font-size:12.5px;color:#475569;font-weight:700}
97    .pf-meta{font-size:13px;color:#475569;margin-bottom:5px}
98    .pf-meta a{color:#1d4ed8}
99    .pf-bio{font-size:13px;color:#334155;background:#f7f8fa;border:1px solid #eef0f3;border-radius:8px;padding:9px 11px;margin:8px 0 2px;white-space:pre-wrap;overflow-wrap:anywhere}
100    .pf-sech{font-weight:800;color:#1d4ed8;font-size:14px;margin:16px 0 9px;border-top:1px solid #eef0f3;padding-top:14px}
101    .pf-app{background:#fff;border:1px solid #e6ebf5;border-radius:9px;padding:9px 11px;margin-bottom:7px}
102    .pf-app-top{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
103    .pf-org{font-weight:700;color:#1f2a44;font-size:13.5px}
104    .pf-score{font-weight:700;color:#1d4ed8;font-size:12px;margin-left:6px}
105    .pf-status{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;border-radius:999px;padding:2px 8px;white-space:nowrap}
106    .pf-status.pending{color:#92400e;background:#fef3c7}
107    .pf-status.approved{color:#065f46;background:#d1fae5}
108    .pf-link{font-size:12px;color:#1d4ed8;font-weight:700;text-decoration:none;margin-top:6px;display:inline-block}
109    @media (max-width:640px){ .pf-link{display:inline-flex;align-items:center;min-height:44px;margin-top:0} }
110    .pf-note{font-size:12px;color:#475569;font-style:italic;margin-top:5px}
111    .pf-sub{font-weight:700;font-size:12.5px;margin-bottom:6px}
112    .pf-empty{font-size:13px;color:#9aa1ab}
113    .fld{margin-bottom:10px}
114    .updlist{display:flex;flex-direction:column;gap:8px}
115    .updrow{display:flex;align-items:center;gap:12px;background:#fff;border:1px solid #e3e7ef;border-radius:10px;padding:10px 13px}
116    .updrow-main{flex:1;min-width:0}
117    .updrow-t{font-weight:700;color:#1f2a44;font-size:14px}
118    .updtag{font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;border-radius:999px;
119      padding:2px 7px;background:#fdecec;color:#b8232a;border:1px solid #f5c9c9;vertical-align:1px}
120    .updtag.draft{background:#eef1f5;color:#6b7280;border-color:#dfe3ea}
121    .fld label{display:block;font-size:12px;font-weight:700;color:#475569;margin-bottom:3px}
122    .fld input,.fld select{width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box}
123    .fhint{font-size:11.5px;color:#64748b;margin-top:4px}
124    .modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:6px}
125    .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:#1f2a44;color:#fff;padding:10px 16px;border-radius:10px;font-size:13px;font-weight:600;box-shadow:0 8px 24px rgba(0,0,0,.25);opacity:0;transition:opacity .2s;z-index:600;pointer-events:none}
126    .toast.show{opacity:1}
127  
128    .rsvpbox{background:#fff;border:1px solid var(--line,#e3e7ef);border-radius:16px;box-shadow:0 6px 20px rgba(20,25,45,.06);padding:16px 18px;margin:0 0 18px}
129    .rsvphead{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}
130    .rsvpbox h2{margin:0;font-size:15px;color:#1f2a44}
131    .rsvpwhen{font-size:13px;color:#6b7280;margin-top:2px}
132    .rsvpacts{display:flex;gap:7px;flex-wrap:wrap}
133    .rsvpcounts{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0 10px}
134    .rsvpchip{font-size:12px;font-weight:700;border-radius:999px;padding:5px 12px;background:#eef1f5;color:#6b7280}
135    .rsvpchip.yes{background:#dcfce7;color:#166534}
136    .rsvpchip.no{background:#fee2e2;color:#991b1b}
137    .rsvpchip.maybe{background:#fef3c7;color:#92400e}
138    .rsvplist{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:7px}
139    .rsvprow{display:flex;justify-content:space-between;align-items:center;gap:8px;border:1px solid #eef1f7;
140      border-radius:10px;padding:8px 11px;font-size:13px}
141    .rsvprow b{font-weight:600;color:#1f2a44}
142    .rsvptag{font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.03em;border-radius:999px;padding:2px 8px}
143    .rsvptag.yes{background:#dcfce7;color:#166534}
144    .rsvptag.no{background:#fee2e2;color:#991b1b}
145    .rsvptag.maybe{background:#fef3c7;color:#92400e}
146    .rsvptag.none{background:#eef1f5;color:#8a93a0}
147    .rsvpnote{font-size:12px;color:#6b7280;font-style:italic;margin-top:2px}
148    @media(max-width:560px){ .rsvplist{grid-template-columns:1fr} }
149    /* Board items (the board room's staff panel). Every control is a 44px target; the dialogs scroll inside themselves. */
150    #biBox .rsvpacts .btn,#biBox .bi-rowacts .btn,.bi-modal .btn{min-height:44px;font-size:13px;padding:8px 14px}
151    .bi-row{flex-wrap:wrap}
152    .bi-row .updrow-main{flex:1 1 260px}
153    .bi-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;color:#475569;font-weight:700}
154    .bi-rowacts{display:flex;gap:7px;flex-wrap:wrap}
155    .bi-msg{font-size:13px;font-weight:600;border-radius:10px;padding:10px 12px;margin:10px 0;overflow-wrap:anywhere}
156    .bi-msg.bi-ok{background:#e9f4ee;border:1px solid #c2e0d0;color:#14532d}
157    .bi-msg.bi-bad{background:#fdecec;border:1px solid #f3c3c3;color:#8d1c1c}
158    .bi-msg.bi-info{background:#edf2fd;border:1px solid #c8d7f5;color:#1b3a78}
159    .bi-modal{max-width:640px}
160    .bi-modal .modal-actions,.bi-rowacts{flex-wrap:wrap}
161    .bi-modal .btn,#biBox .btn{white-space:normal;text-align:center}
162    .bi-modal fieldset{border:0;padding:0;margin:0 0 12px;min-width:0}
163    .bi-modal legend{font-size:13px;font-weight:800;color:#1f2a44;padding:0;margin:0 0 6px}
164    .bi-choice{display:flex;align-items:center;gap:9px;min-height:44px;padding:6px 10px;border:1px solid #e3e7ef;border-radius:10px;margin:0 0 6px;font-size:13.5px;color:#1f2a44;cursor:pointer}
165    .bi-choice input{width:18px;height:18px;margin:0;flex:none}
166    .bi-rule{font-size:13.5px;color:#1b3a78;background:#edf2fd;border:1px solid #c8d7f5;border-radius:10px;padding:10px 12px;margin:0 0 12px}
167    .bi-modal textarea{width:100%;font:inherit;font-size:13.5px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box;min-height:88px}
168    .bi-modal input[type=datetime-local],.bi-modal input[type=date]{width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box;min-height:44px}
169    .bi-modal h4{margin:16px 0 6px;font-size:13.5px;color:#1f2a44}
170    .bi-preview{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;margin:12px 0}
171    @media(max-width:620px){ .bi-preview{grid-template-columns:minmax(0,1fr)} }
172    .bi-cap{font-size:11px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#64748b;margin:0 0 6px}
173    .bi-phone{border:2px solid #1f2a44;border-radius:22px;padding:12px;max-width:340px;background:#f7f5f4;font-size:13px;line-height:1.45;overflow-wrap:anywhere}
174    .bi-phone p{margin:0 0 6px}
175    .bi-ph-head{background:#fff;border-left:4px solid #b4232b;border-radius:10px;padding:8px 10px;margin:0 0 8px}
176    .bi-ph-title{font-weight:800;font-size:15px;color:#1f2a44}
177    .bi-ph-words,.bi-mailtext{white-space:pre-wrap;background:#fff;border:1px solid #e3e7ef;border-radius:8px;padding:8px 10px;margin:0 0 6px}
178    .bi-mail{border:1px solid #e3e7ef;border-radius:12px;padding:12px;background:#fff;font-size:13px;line-height:1.45;overflow-wrap:anywhere;min-width:0}
179    .bi-mail p{margin:0 0 6px}
180    .bi-sure{border-top:1px solid #eef0f3;margin-top:12px;padding-top:12px}
181    .bi-sure p{font-size:14px;line-height:1.5;color:#1f2a44;margin:0 0 10px}
182    .bi-facts{font-size:13px;color:#475569;line-height:1.55;margin:0 0 10px}
183    .bi-facts p{margin:0 0 3px}
184    .bi-docs{margin:0 0 10px;padding-left:18px;font-size:13px}
185    .bi-comment{border-top:1px solid #f1f3f6;padding:8px 0;font-size:13px}
186    .bi-comment .bi-ctext{white-space:pre-wrap;overflow-wrap:anywhere}
187    .bi-comment summary{cursor:pointer;min-height:44px;display:flex;align-items:center;color:#6b7280;font-weight:700}
188    .bi-entries{font-size:12.5px;color:#475569;padding-left:18px;margin:6px 0}
189    .bi-every>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:700;color:#1f2a44;font-size:13px}
190  </style>
191  
191<script src="auth.js?v=73daad4c41"></script>
191
192  
192<script defer src="navbadge.js?v=8bba535fd2"></script>
192
193  <link rel="stylesheet" href="rrcc-ui.css?v=60e872b858" />
194  
194<script defer src="rrcc-ui.js?v=5e7ec569a3"></script>
194
195  <link rel="stylesheet" href="board-room.css?v=6abd6c4839">
196  
196<script defer src="board-decisions-page.js?v=c8b41735db"></script>
196
197</head>
198<body>
199  <header class="sitehdr"><a class='logolink' href='/'><img class="sitelogo" src="assets/rrcc-logo.png" alt="Red River Community Corps"></a><button id="featBtn" type="button" class="hdrcta">Request a feature</button></header>
200  <nav class="progtabs">
201    <a class='progtab' href='/'>Oklahoma Opportunities</a>
202    <a class='progtab' href='/vista-dashboard'>VISTA</a>
203    <a class='progtab active' href='/board-members'>Board Members</a>
204    <a class='feattab' href='/jobs'>Service Opportunities</a>
205  </nav>
206  <nav class="topnav">
207    <div class="fitline" style="font-weight:800;color:#1f2a44">RRCC Board of Directors</div>
208  </nav>
209
210  <main class="wrap">
211    <!-- Board items: the board room's staff panel (DESIGN.md "pagesAndEmails" page 4). Staff prepare, preview and open items
212         for the board room and see where each stands. Items about the Executive Director, Executive Committee items and
213         items for directors only never appear here (the board-items route leaves them out, titles included). -->
214    <section class="rsvpbox" id="biBox" aria-labelledby="biHead">
215      <div class="rsvphead">
216        <div>
217          <h2 id="biHead">Board items</h2>
218          <div class="rsvpwhen">Items for the board room, the official record of what directors read, said and decided. Once an item is open it cannot be changed or deleted, only added to. Items about the Executive Director never appear here.</div>
219        </div>
220        <div class="rsvpacts">
221          <button class="btn sm primary" id="biNew" type="button">Prepare an item</button>
222          <button class="btn sm" id="biCheck" type="button">Open the address check for all directors</button>
223        </div>
224      </div>
225      <div id="biMsg"></div>
226      <div class="updlist" id="biList"><div class="rsvpwhen">Loading.</div></div>
227    </section>
228
229    <section class="rsvpbox" id="updBox">
230      <div class="rsvphead">
231        <div>
232          <h2>Updates for the board</h2>
233          <div class="rsvpwhen">Posted to every board member's own dashboard. They see these the moment you publish.</div>
234        </div>
235        <div class="rsvpacts">
236          <button class="btn sm" id="updNew" type="button">Post an update</button>
237        </div>
238      </div>
239      <div class="updlist" id="updList"><div class="rsvpwhen">Loading.</div></div>
240    </section>
241
242    <section class="rsvpbox" id="minBox">
243      <div class="rsvphead">
244        <div>
245          <h2>Meeting minutes</h2>
246          <div class="rsvpwhen">Once the board approves a set of minutes, record it here and send it to the Secretary to sign in DocuSign. When the envelope completes this marks itself signed and files the signed copy on every board member's dashboard. While anything sits unsigned you get an email once a week.</div>
247        </div>
248        <div class="rsvpacts">
249          <button class="btn sm" id="minNew" type="button">Record approved minutes</button>
250        </div>
251      </div>
252      <div class="updlist" id="minList"><div class="rsvpwhen">Loading.</div></div>
253      <div class="rsvpwhen" id="minUnmatched" style="margin-top:10px"></div>
254    </section>
255
256  <div class="ov" id="minOv" hidden>
257    <div class="modal" role="dialog" aria-modal="true" aria-labelledby="minModalTitle">
258      <h3 id="minModalTitle">Record approved minutes</h3>
259      <div class="fld"><label for="minDate">Which meeting</label>
260        <input id="minDate" type="text" placeholder="e.g. May 26, 2026" maxlength="80">
261        <div class="rsvpwhen" style="margin-top:4px">The meeting the minutes cover, not today's date. This is what gets matched to the DocuSign envelope, so a month and a year need to be in there.</div>
262      </div>
263      <div class="fld"><label for="minTitle">Title (optional)</label>
264        <input id="minTitle" type="text" placeholder="May 2026 Board Meeting Minutes" maxlength="200"></div>
265      <div class="fld"><label for="minNote">Note (optional)</label>
266        <input id="minNote" type="text" placeholder="Anything worth remembering about this approval" maxlength="2000"></div>
267      <div id="minMsg" style="display:none;font-size:13px;font-weight:600;margin-bottom:8px"></div>
268      <div class="modal-actions">
269        <button class="btn" id="minCancel" type="button">Cancel</button>
270        <button class="btn primary" id="minSave" type="button">Record it</button>
271      </div>
272    </div>
273  </div>
274
275  <div class="ov" id="updOv" hidden>
276    <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="updModalTitle">
277      <h3 id="updModalTitle">Post an update</h3>
278      <div class="fld"><label for="updTitle">Title</label><input id="updTitle" type="text" placeholder="e.g. 2025-2026 Annual Report" maxlength="200"></div>
279      <div class="fld"><label for="updBody">Message</label>
279<textarea id="updBody" rows="7" style="width:100%;font:inherit;font-size:13px;padding:8px 10px;border:1px solid #ccd1d9;border-radius:8px;box-sizing:border-box" placeholder="What the board should know." aria-describedby="updPrivacy"></textarea><p id="updPrivacy" style="margin:6px 0 0;font-size:12.5px;color:#5f5650">Board members open updates by their link, without a password. Leave out member names, contact details and case notes.</p></div>
280      <div class="fld"><label for="updLink">Link (optional)</label>
281        <input id="updLink" type="url" placeholder="https://rrccdashboard.netlify.app/board-docs/report.pdf">
282        <div class="rsvpwhen" style="margin-top:4px">Use this for anything large. A PDF hosted on this site opens right on their page. No size limit, unlike an upload.</div>
283      </div>
284      <div class="fld"><label for="updLinkLabel">What to call the link (optional)</label>
285        <input id="updLinkLabel" type="text" placeholder="2025 Annual Report" maxlength="120"></div>
286      <div class="fld"><label for="updFile">Attachment (optional, about 4 MB max)</label>
287        <input id="updFile" type="file" accept=".pdf,.doc,.docx,.xlsx,.ppt,.pptx,.png,.jpg,.jpeg">
288        <div class="rsvpwhen" id="updFileNow" style="margin-top:4px"></div>
289      </div>
290      <label style="display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600;color:#475569;margin:4px 0 10px">
291        <input type="checkbox" id="updPin"> Pin to the top of their page
292      </label>
293      <div id="updMsg" style="display:none;font-size:13px;font-weight:600;margin-bottom:8px"></div>
294      <div class="modal-actions">
295        <button class="btn" id="updDelete" type="button" hidden style="margin-right:auto;color:#b8232a;border-color:#e7b9b9">Remove</button>
296        <button class="btn" id="updCancel" type="button">Cancel</button>
297        <button class="btn primary" id="updSave" type="button">Publish</button>
298      </div>
299    </div>
300  </div>
301    <section class="rsvpbox" id="rsvpBox" hidden>
302      <div class="rsvphead">
303        <div>
304          <h2 id="rsvpTitle">Meeting RSVPs</h2>
305          <div class="rsvpwhen" id="rsvpWhen"></div>
306        </div>
307        <div class="rsvpacts">
308          <button class="btn sm" id="rsvpCopy" type="button">Copy RSVP link</button>
309          <button class="btn sm" id="rsvpSet" type="button">Set the meeting</button>
310        </div>
311      </div>
312      <div class="rsvpcounts" id="rsvpCounts"></div>
313      <div class="rsvplist" id="rsvpList"></div>
314    </section>
315
316    <div class="head">
317      <h1>Board Members</h1>
318      <p>The RRCC Board of Directors. Email a member or a selected group (opens a draft in your own email app, everyone in To:), and send each member a private link to confirm their info and add a headshot. Click a member to edit their record.</p>
319    </div>
320    <div class="countbar" id="counts"></div>
321    <div class="toolbar">
322      <input type="search" id="q" placeholder="Search name, position, or email…" />
323      <button class="btn" id="addBtn" type="button">+ Add member</button>
324      <label><input type="checkbox" id="showArchived" /> Show archived</label>
325    </div>
326    <div class="actionbar">
327      <label><input type="checkbox" id="selAll" /> <span style="font-weight:700">Select all</span></label>
328      <span class="sel" id="selCount">0 selected</span>
329      <button class="btn primary" id="emailSel" type="button" disabled>Email selected</button>
330      <button class="btn" id="linkSel" type="button" disabled>Send profile links to selected</button>
331    </div>
332    <div id="rotation"></div>
333    <details class="actlog" id="actlog"><summary>Board activity log</summary><div class="actlist" id="actlist"><p class="actempty">Loading.</p></div></details>
334    <div class="grid" id="list"><p class="empty">Loading…</p></div>
335  </main>
336
337  <div class="ov" id="editOv" hidden>
338    <div class="modal" role="dialog" aria-modal="true" aria-labelledby="editTitle">
339      <h3 id="editTitle">Edit member</h3>
340      <div class="fld"><label>Name</label><input id="f_name" type="text" /></div>
341      <div class="fld"><label>Position</label>
342        <select id="f_position">
343          <option>Board Chair</option><option>Vice Chair</option><option>Board Treasurer</option><option>Board Secretary</option><option>Board of Directors</option>
344        </select></div>
345      <div class="fld"><label>Term <span style="font-weight:600;color:#9aa1ab">(auto — every 2 years from the start date)</span></label><input id="f_term" type="text" disabled style="background:#f3f5f9;color:#9aa1ab" /></div>
346      <div class="fld"><label>Initial start</label><input id="f_start" type="text" placeholder="Jan 2024" /></div>
347      <div class="fld"><label>Final term end</label><input id="f_end" type="text" placeholder="Jan 2030" /></div>
348      <div class="fld"><label>Email</label><input id="f_email" type="email" />
349        <div class="fhint" id="f_email_hint" hidden>Changing this email does not change where this director's sign-in codes go.</div></div>
350      <div class="modal-actions">
351        <button class="btn" id="editCancel" type="button">Cancel</button>
352        <button class="btn primary" id="editSave" type="button">Save</button>
353      </div>
354    </div>
355  </div>
356
357  <!-- Profile: a STAFF-SIDE view of the member, built from the roster (2026-09-30). It used to open the member's own
358       dashboard with their private link, which is the member's key to the portal; staff never hold that link now. -->
359  <div class="ov" id="profileOv" hidden>
360    <div class="modal wide" role="dialog" aria-modal="true" aria-labelledby="pfTitle">
361      <div id="pfBody"></div>
362      <div class="modal-actions">
363        <button class="btn" id="pfClose" type="button">Close</button>
364      </div>
365    </div>
366  </div>
367
368  <!-- Board items: Prepare (the two questions, the rule, the words, the documents, the deadline, the help contact),
369       Preview (the phone view and the email) and Open; then one item's later steps. -->
370  <div class="ov" id="biPrepOv" hidden>
371    <div class="modal wide bi-modal" role="dialog" aria-modal="true" aria-labelledby="biPrepTitle">
372      <h3 id="biPrepTitle">Prepare a board item</h3>
373      <fieldset>
374        <legend>What do you need from the board?</legend>
375        <label class="bi-choice"><input type="radio" name="biNeed" value="read"> Just read it</label>
376        <label class="bi-choice"><input type="radio" name="biNeed" value="comments"> Comments</label>
377        <label class="bi-choice"><input type="radio" name="biNeed" value="meeting"> A decision at the next meeting</label>
378        <label class="bi-choice"><input type="radio" name="biNeed" value="vote"> A vote before the meeting</label>
379      </fieldset>
380      <fieldset>
381        <legend>What is it about?</legend>
382        <label class="bi-choice"><input type="radio" name="biAbout" value="budget"> Budget</label>
383        <label class="bi-choice"><input type="radio" name="biAbout" value="policy"> Policy</label>
384        <label class="bi-choice"><input type="radio" name="biAbout" value="minutes"> Minutes</label>
385        <label class="bi-choice"><input type="radio" name="biAbout" value="bylaws"> Bylaws</label>
386        <label class="bi-choice"><input type="radio" name="biAbout" value="other"> Other</label>
387      </fieldset>
388      <p class="bi-rule" id="biRule">Answer both questions, and the rule for this item shows here in one sentence.</p>
389      <fieldset id="biBudgetFields" hidden>
390        <legend>Budget pay checks</legend>
391        <p>
391Does this budget change the Executive Director's pay or benefits?</p>
392        <label class="bi-choice"><input type="radio" name="biBudgetPay" value="yes"> Yes</label>
393        <label class="bi-choice"><input type="radio" name="biBudgetPay" value="no"> No</label>
394        <p class="fhint">A budget that changes that pay must exclude the Executive Director's pay from its motion. The separate private pay decision handles it.</p>
395        <div class="fld"><label for="biStaffPayBasis">Basis for staff pay in this budget</label><textarea id="biStaffPayBasis" maxlength="1000" rows="3" placeholder="Briefly explain the basis for staff pay."></textarea></div>
396      </fieldset>
397      <div class="fld"><label for="biTitle">Title</label><input id="biTitle" type="text" maxlength="200" placeholder="e.g. Read the 2027 board calendar"></div>
398      <div class="fld"><label for="biWords" id="biWordsLabel">The exact words</label><textarea id="biWords" rows="5" maxlength="4000" placeholder="What directors read. For a decision, the motion word for word."></textarea></div>
399      <div class="fld"><label for="biFiles">Documents (optional)</label><input id="biFiles" type="file" multiple accept=".pdf,.docx,.xlsx,.png,.jpg,.jpeg"><div class="fhint" id="biFilesNow">PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each.</div></div>
400      <div class="fld"><label for="biDeadline" id="biDeadlineLabel">Deadline, Central time</label><input id="biDeadline" type="datetime-local"></div>
401      <div class="fld" id="biMeetingFld" hidden><label for="biMeeting">Meeting date</label><input id="biMeeting" type="date"></div>
402      <div class="fld"><label for="biHelp">Help contact (optional)</label><input id="biHelp" type="email" maxlength="254" placeholder="Leave blank for Nicky Kazerooni"><div class="fhint">A staff address from People and Routing. Directors are told to ask this person for help.</div></div>
403      <div id="biPrepMsg"></div>
404      <div id="biPreview"></div>
405      <div class="modal-actions" id="biPrepActs">
406        <button class="btn" id="biPrepCancel" type="button">Cancel</button>
407        <button class="btn" id="biPreviewBtn" type="button">Preview</button>
408        <button class="btn primary" id="biOpenBtn" type="button">Open the item</button>
409      </div>
410      <div class="bi-sure" id="biSure" hidden>
411        <p id="biSureText"></p>
412        <div class="modal-actions">
413          <button class="btn" id="biSureBack" type="button">Go back</button>
414          <button class="btn primary" id="biSureOpen" type="button">Open it</button>
415        </div>
416      </div>
417    </div>
418  </div>
419
420  <div class="ov" id="biItemOv" hidden>
421    <div class="modal wide bi-modal" role="dialog" aria-modal="true" aria-labelledby="biItemTitle">
422      <h3 id="biItemTitle">Board item</h3>
423      <div id="biItemBody"></div>
424      <div class="modal-actions">
425        <button class="btn" id="biItemClose" type="button">Close</button>
426      </div>
427    </div>
428  </div>
429
430  <div class="toast" id="toast"></div>
431
432  
432<script>
433  (function(){
434    var FN="/.netlify/functions";
435    var MEM=[], SEL={}, ROT={order:[],nextId:null,nextName:""}, editingId=null;
436    function $(s){ return document.querySelector(s); }
437    function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c];}); }
438    // ---- Meeting minutes, on their way to the Secretary's signature ------------
439    var MIN=[];
440    function minMsg(t,err){ var m=document.getElementById("minMsg"); if(!m) return;
441      m.textContent=t||""; m.style.display=t?"block":"none"; m.style.color=err?"#b8232a":"#177245"; }
442    function minWhen(at){ return at?new Date(at).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):""; }
443
444    function renderMinutes(){
445      var box=document.getElementById("minList"); if(!box) return;
446      if(!MIN.length){ box.innerHTML='<div class="rsvpwhen">Nothing recorded yet. After the board votes to approve a set of minutes, record it here so it does not get forgotten.</div>'; return; }
447      box.innerHTML=MIN.map(function(m){
448        var pill = m.signedAt
449          ? '<span class="updtag" style="background:#e6f4ea;color:#177245">Signed</span>'
450          : (m.status==="sent"
451              ? '<span class="updtag" style="background:#fff4e0;color:#8a5a00">Out for signature</span>'
452              : '<span class="updtag" style="background:#fdecec;color:#b8232a">Not sent yet</span>');
453        var sub = m.signedAt
454          ? ("Signed "+esc(minWhen(m.signedAt))+(m.signedBy?(" by "+esc(m.signedBy)):""))
455          : ("Approved "+esc(minWhen(m.approvedAt))+(m.approvedBy?(" &middot; "+esc(m.approvedBy)):"")
456             + (m.remindCount?(" &middot; reminded "+m.remindCount+"x"):""));
457        var acts = m.signedAt ? "" :
458          '<button class="btn sm" data-minsent="'+esc(m.id)+'" type="button">'+(m.status==="sent"?"Not sent after all":"Mark sent for signature")+'</button>';
459        return '<div class="updrow"><div class="updrow-main">'
460          + '<div class="updrow-t">'+esc(m.title||m.meetingDate)+' '+pill+'</div>'
461          + '<div class="rsvpwhen">'+sub+'</div></div>'
462          + acts
463          + '<button class="btn sm" data-minrm="'+esc(m.id)+'" type="button" style="color:#b8232a;border-color:#e7b9b9">Remove</button></div>';
464      }).join("");
465      box.querySelectorAll("[data-minsent]").forEach(function(b){
466        b.onclick=function(){ var id=b.getAttribute("data-minsent");
467          var rec=MIN.filter(function(x){return x.id===id;})[0];
468          minPost({action:(rec&&rec.status==="sent")?"markMinutesUnsent":"markMinutesSent", id:id}); };
469      });
470      box.querySelectorAll("[data-minrm]").forEach(function(b){
471        b.onclick=function(){ var id=b.getAttribute("data-minrm");
472          var rec=MIN.filter(function(x){return x.id===id;})[0];
473          if(!confirm("Remove the record for "+((rec&&(rec.title||rec.meetingDate))||"these minutes")+"? This does not touch anything in DocuSign.")) return;
474          minPost({action:"removeMinutes", id:id}); };
475      });
476    }
477
478    function applyMinutes(d){
479      var box=document.getElementById("minList");
480      if(!d||!d.ok){ if(box) box.innerHTML='<div class="rsvpwhen">'+esc((d&&d.error)||"Could not load the minutes.")+'</div>'; return; }
481      MIN=d.minutes||[]; renderMinutes();
482      var u=document.getElementById("minUnmatched");
483      if(u) u.innerHTML=(d.unmatched&&d.unmatched.length)
484        ? ('A signed envelope came back that could not be matched to one meeting: '
485           + esc(d.unmatched[0].subject||"(no subject)")
486           + '. Record those minutes above, then re-send, or ask for it to be attached by hand.')
487        : "";
488    }
489    function loadMinutes(){
490      fetch(FN+"/board-members?minutes=1",{cache:"no-store"})
491        .then(function(r){return r.json();})
492        .then(applyMinutes)
493        .catch(function(){ var box=document.getElementById("minList"); if(box) box.innerHTML='<div class="rsvpwhen">Could not load the minutes.</div>'; });
494    }
495
496    function minPost(body){
497      return post(body).then(function(d){
498        if(!d||!d.ok){ alert((d&&d.error)||"That did not save."); return d; }
499        loadMinutes(); return d;
500      });
501    }
502
503    function wireMinutes(){
504      var nb=document.getElementById("minNew"), ov=document.getElementById("minOv");
505      if(nb) nb.onclick=function(){
506        ["minDate","minTitle","minNote"].forEach(function(id){ var e=document.getElementById(id); if(e) e.value=""; });
507        minMsg(""); if(ov) ov.hidden=false; var f=document.getElementById("minDate"); if(f) f.focus();
508      };
509      var cancel=document.getElementById("minCancel");
510      if(cancel) cancel.onclick=function(){ if(ov) ov.hidden=true; };
511      var save=document.getElementById("minSave");
512      if(save) save.onclick=function(){
513        var date=(document.getElementById("minDate")||{}).value||"";
514        if(!date.trim()){ minMsg("Put in which meeting these minutes are for.",true); return; }
515        save.disabled=true; minMsg("Saving.");
516        minPost({action:"recordMinutes", meetingDate:date,
517          title:(document.getElementById("minTitle")||{}).value||"",
518          note:(document.getElementById("minNote")||{}).value||""})
519          .then(function(d){ save.disabled=false;
520            if(d&&d.ok){ if(ov) ov.hidden=true; minMsg(""); } else { minMsg((d&&d.error)||"That did not save.",true); } })
521          .catch(function(){ save.disabled=false; minMsg("That did not save.",true); });
522      };
523    }
524
525    // ---- Updates for the board -------------------------------------------------
526    var UPD=[], UPDEDIT=null, UPDFILE=null;
527    function updMsg(t,err){ var m=document.getElementById("updMsg"); if(!m) return;
528      m.textContent=t||""; m.style.display=t?"block":"none"; m.style.color=err?"#b8232a":"#177245"; }
529    function updWhen(at){ return at?new Date(at).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):""; }
530
531    function renderUpdates(){
532      var box=document.getElementById("updList"); if(!box) return;
533      if(!UPD.length){ box.innerHTML='<div class="rsvpwhen">Nothing posted yet. The board sees this section on their own dashboard as soon as you publish something.</div>'; return; }
534      var list=UPD.slice().sort(function(a,b){
535        if(!!b.pinned !== !!a.pinned) return b.pinned?1:-1;
536        return (b.at||0)-(a.at||0);
537      });
538      box.innerHTML=list.map(function(u){
539        return '<div class="updrow" data-upd="'+esc(u.id)+'">'
540          + '<div class="updrow-main"><div class="updrow-t">'+esc(u.title)
541            + (u.pinned?' <span class="updtag">Pinned</span>':'')
542            + (u.draft?' <span class="updtag draft">Draft</span>':'')+'</div>'
543          + '<div class="rsvpwhen">'+esc(updWhen(u.at))+(u.by?(" &middot; "+esc(u.by)):"")
544            + (u.doc?(" &middot; "+esc(u.doc.filename)):"")+'</div></div>'
545          + '<button class="btn sm" data-updedit="'+esc(u.id)+'" type="button">Edit</button></div>';
546      }).join("");
547      box.querySelectorAll("[data-updedit]").forEach(function(b){
548        b.onclick=function(){ openUpd(UPD.filter(function(x){return x.id===b.getAttribute("data-updedit");})[0]); };
549      });
550    }
551
552    function applyUpdates(d){
553      if(!d||!d.ok){ document.getElementById("updList").innerHTML='<div class="rsvpwhen">'+esc((d&&d.error)||"Could not load the updates.")+'</div>'; return; }
554      UPD=d.updates||[]; renderUpdates();
555    }
556    function loadUpdates(){
557      fetch(FN+"/board-members?updates=1",{cache:"no-store"})
558        .then(function(r){return r.json();})
559        .then(applyUpdates)
560        .catch(function(){ document.getElementById("updList").innerHTML='<div class="rsvpwhen">Could not load the updates.</div>'; });
561    }
562
563    function openUpd(u){
564      UPDEDIT=u||null; UPDFILE=null; updMsg("");
565      document.getElementById("updModalTitle").textContent=u?"Edit update":"Post an update";
566      document.getElementById("updTitle").value=u?(u.title||""):"";
567      document.getElementById("updLink").value=u?(u.link||""):"";
568      document.getElementById("updLinkLabel").value=u?(u.linkLabel||""):"";
569      document.getElementById("updBody").value=u?(u.body||""):"";
570      document.getElementById("updPin").checked=u?!!u.pinned:false;
571      document.getElementById("updFile").value="";
572      document.getElementById("updFileNow").textContent=(u&&u.doc)?("Attached: "+u.doc.filename+". Choosing a new file replaces it."):"";
573      document.getElementById("updDelete").hidden=!u;
574      document.getElementById("updSave").textContent=u?"Save changes":"Publish";
575      document.getElementById("updOv").hidden=false;
576    }
577    function closeUpd(){ document.getElementById("updOv").hidden=true; UPDEDIT=null; UPDFILE=null; }
578
579    function saveUpd(){
580      var title=document.getElementById("updTitle").value.trim();
581      if(!title){ updMsg("Give the update a title.",true); return; }
582      var btn=document.getElementById("updSave"); btn.disabled=true; btn.textContent="Saving…";
583      var payload={ action: UPDEDIT?"editUpdate":"publishUpdate", id: UPDEDIT?UPDEDIT.id:undefined,
584        title:title, body:document.getElementById("updBody").value, pinned:document.getElementById("updPin").checked,
585        link:document.getElementById("updLink").value, linkLabel:document.getElementById("updLinkLabel").value };
586      if(UPDFILE){ payload.docBase64=UPDFILE.b64; payload.filename=UPDFILE.name; }
587      post(payload).then(function(d){
588        if(!d||!d.ok){ updMsg((d&&d.error)||"Could not save that.",true); btn.disabled=false; btn.textContent=UPDEDIT?"Save changes":"Publish"; return; }
589        closeUpd(); loadUpdates();
590      }).catch(function(){ updMsg("Could not save that.",true); btn.disabled=false; btn.textContent=UPDEDIT?"Save changes":"Publish"; });
591    }
592
593    function wireUpdates(){
594      var nb=document.getElementById("updNew"); if(nb) nb.onclick=function(){ openUpd(null); };
595      var cb=document.getElementById("updCancel"); if(cb) cb.onclick=closeUpd;
596      var sv=document.getElementById("updSave"); if(sv) sv.onclick=saveUpd;
597      var del=document.getElementById("updDelete");
598      if(del) del.onclick=function(){
599        if(!UPDEDIT) return;
600        if(!confirm("Remove \""+UPDEDIT.title+"\"? The board will no longer see it.")) return;
601        del.disabled=true;
602        post({action:"removeUpdate",id:UPDEDIT.id}).then(function(d){
603          del.disabled=false;
604          if(!d||!d.ok){ updMsg((d&&d.error)||"Could not remove that.",true); return; }
605          closeUpd(); loadUpdates();
606        }).catch(function(){ del.disabled=false; updMsg("Could not remove that.",true); });
607      };
608      var f=document.getElementById("updFile");
609      if(f) f.onchange=function(){
610        var file=f.files&&f.files[0]; if(!file){ UPDFILE=null; return; }
611        // ~5 MB decoded is the server cap; catch it here so nobody waits on an upload
612        // that is going to be refused.
613        if(file.size>5*1024*1024){ updMsg("That file is larger than 5 MB. Try a smaller export.",true); f.value=""; UPDFILE=null; return; }
614        var rd=new FileReader();
615        rd.onload=function(){
616          UPDFILE={ name:file.name, b64:String(rd.result).split(",")[1]||"" };
617          document.getElementById("updFileNow").textContent="Ready to attach: "+file.name;
618          updMsg("");
619        };
620        rd.readAsDataURL(file);
621      };
622    }
623
624    function post(payload){ return fetch(FN+"/board-members",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(payload||{})}).then(function(r){return r.json();}); }
625    var toastT; function toast(m,ms){ var t=$("#toast"); t.textContent=m; t.classList.add("show"); clearTimeout(toastT); toastT=setTimeout(function(){t.classList.remove("show");},ms||2800); }
626    // A roster change is also written to the board record and the officers are emailed (board room, 2026-10-01). The
627    // change is saved either way; this says when the record is behind or someone was not told.
628    function rosterNote(d){
629      var out=[];
630      if(d&&d.recordWarning) out.push(String(d.recordWarning));
631      if(d&&d.notice&&d.notice.of&&d.notice.sent<d.notice.of&&d.notice.message) out.push(String(d.notice.message));
632      return out.join(" ");
633    }
634    function initials(n){ n=String(n||"").trim().split(/\s+/); return ((n[0]||"")[0]||"")+((n[1]||"")[0]||""); }
635    function posClass(p){ p=String(p||"").toLowerCase(); if(p.indexOf("chair")>=0&&p.indexOf("vice")<0)return"p-chair"; if(p.indexOf("vice")>=0)return"p-vice"; if(p.indexOf("treasurer")>=0)return"p-treasurer"; if(p.indexOf("secretary")>=0)return"p-secretary"; return"p-dir"; }
636    // Term is derived from the start date: every 2 years = one term (capped at the final-term-end span).
637    function parseDate(s){ if(!s) return null; var d=new Date(s); return isNaN(d.getTime())?null:d; }
638    function computeTerm(m){
639      var s=parseDate(m.start); if(!s) return m.term||"—";
640      var yrs=(Date.now()-s.getTime())/(365.25*864e5); if(yrs<0) return 1;
641      var t=Math.floor(yrs/2)+1;
642      var e=parseDate(m.end); if(e){ var total=Math.max(1,Math.round((e.getTime()-s.getTime())/(365.25*864e5)/2)); if(t>total) t=total; }
643      return t;
644    }
645    function photoUrl(m){
646      var u=FN+"/board-members?photo=1&id="+encodeURIComponent(m.id);
647      return m.photoVersion ? (u+"&v="+encodeURIComponent(m.photoVersion)) : u;
648    }
649
650    function mget(id){ return MEM.find(function(m){return m.id===id;}); }
651    function visible(){ var q=($("#q").value||"").toLowerCase().trim(); var showRem=$("#showArchived").checked;
652      return MEM.filter(function(m){ if(m.removed&&!showRem) return false; if(!q) return true;
653        return (m.name+" "+m.position+" "+m.email).toLowerCase().indexOf(q)>=0; }); }
654
655    function card(m){
656      var ava = m.hasPhoto ? ('<span class="ava"><img src="'+esc(photoUrl(m))+'" alt="'+esc(m.name)+'" loading="lazy" decoding="async"/></span>') : ('<span class="ava">'+esc(initials(m.name).toUpperCase())+'</span>');
657      var meta=[];
658      meta.push('<a href="mailto:'+esc(m.email)+'">'+esc(m.email)+'</a>');
659      if(m.phone) meta.push(esc(m.phone));
660      if(m.title) meta.push(esc(m.title));
661      var bio = m.bio ? ('<div class="mbiowrap"><div class="mbio clamp">'+esc(m.bio)+'</div><button class="mbiomore" type="button" data-act="biomore" hidden>Show more</button></div>') : "";
662      var upd = m.updatedBy==="self" ? '<div class="updated">Member updated their info</div>' : "";
663      var plsent = m.profileEmailAt ? ('<div class="updated">Profile link sent '+new Date(m.profileEmailAt).toLocaleDateString(undefined,{month:"short",day:"numeric"})+'</div>') : "";
664      // hasLink is all staff are told about a member's private link (true, false, or null when it could not be checked).
665      var nolink = (m.hasLink===false && !m.removed) ? '<div class="nolink">No dashboard link yet. Send profile link makes one and emails it.</div>' : "";
666      // Signed in to the board room with a code (true / false / null when the record could not be read). Never an address.
667      var signed = '<div class="msigned'+(m.signedIn===false?' notyet':'')+'">Signed in: '+(m.signedIn===true?"yes":(m.signedIn===false?"not yet":"not known"))+'</div>';
668      var rv = m.review||{}; var rev = rv.assigned ? ('<div class="mrev">Application reviews: <b>'+rv.assigned+'</b> assigned · '+(rv.pending||0)+' pending · '+(rv.approved||0)+' approved</div>') : "";
669      return '<div class="mcard'+(SEL[m.id]?" sel":"")+'" data-id="'+esc(m.id)+'">'+
670        '<div class="mtop">'+ava+
671          '<div style="flex:1;min-width:0">'+
672            '<div class="mname">'+esc(m.name||"(no name)")+'</div>'+
673            '<span class="mpos '+posClass(m.position)+'">'+esc(m.position||"Board of Directors")+'</span>'+
674            '<div class="mterm">Term '+computeTerm(m)+' · '+esc(m.start||"?")+' – '+esc(m.end||"?")+'</div>'+
675          '</div>'+
676          '<input class="pick" type="checkbox" data-act="pick" data-id="'+esc(m.id)+'"'+(SEL[m.id]?" checked":"")+' title="Select" />'+
677        '</div>'+
678        '<div class="mmeta">'+meta.join("<br>")+'</div>'+
679        bio+upd+plsent+nolink+signed+rev+
680        '<div class="mactions">'+
681          '<button class="btn sm" data-act="profile" data-id="'+esc(m.id)+'">Profile</button>'+
682          '<button class="btn sm" data-act="email" data-id="'+esc(m.id)+'">Email</button>'+
683          '<button class="btn sm" data-act="sendprofile" data-id="'+esc(m.id)+'">Send profile link</button>'+
684          '<button class="btn sm" data-act="resetlink" data-id="'+esc(m.id)+'">Reset link</button>'+
685          '<button class="btn sm" data-act="edit" data-id="'+esc(m.id)+'">Edit</button>'+
686          (m.removed?'<button class="btn sm" data-act="unarchive" data-id="'+esc(m.id)+'">Restore</button>':'<button class="btn sm" data-act="archive" data-id="'+esc(m.id)+'">Archive</button>')+
687        '</div>'+
688      '</div>';
689    }
690
691    function renderCounts(){
692      var active=MEM.filter(function(m){return !m.removed;});
693      var officers=active.filter(function(m){return posClass(m.position)!=="p-dir";}).length;
694      var photos=active.filter(function(m){return m.hasPhoto;}).length;
695      var archived=MEM.filter(function(m){return m.removed;}).length;
696      $("#counts").innerHTML=[
697        '<span class="chip">'+active.length+' members</span>',
698        '<span class="chip">Officers: '+officers+'</span>',
699        '<span class="chip">Headshots on file: '+photos+'</span>'
700      ].concat(archived?['<span class="chip">Archived: '+archived+'</span>']:[]).join("");
701    }
702    function selectedMembers(){ return MEM.filter(function(m){return SEL[m.id]&&!m.removed;}); }
703    function refreshSel(){
704      var n=selectedMembers().length;
705      $("#selCount").textContent=n+" selected";
706      $("#emailSel").disabled=!n; $("#linkSel").disabled=!n;
707    }
708    // Move one member one place and save the whole order - the same shape the drag
709    // handler posts, so the two paths can never disagree about what an order is.
710    function nudgeRotation(id,dir){
711      var order=((ROT&&ROT.order)||[]).map(function(o){return o.id;});
712      var i=order.indexOf(id), j=i+dir;
713      if(i<0||j<0||j>=order.length) return;
714      order.splice(j,0,order.splice(i,1)[0]);
715      post({action:"setRotation",order:order}).then(function(d){
716        if(d&&d.ok&&d.rotation){ ROT=d.rotation; }
717        renderRotation();
718      }).catch(function(){ renderRotation(); });
719    }
720
721    function renderRotation(){
722      var el=$("#rotation"); if(!el) return;
723      var order=(ROT&&ROT.order)||[];
724      if(!order.length){ el.innerHTML=""; return; }
725      // Each row carries move-up / move-down buttons as well as being draggable.
726      // The arrows were removed in July when dragging came in, which left reordering
727      // impossible on a phone - HTML5 drag never fires on a touch screen. They are back,
728      // and they drive the SAME setRotation POST the drag does.
729      var rows=order.map(function(o,i){
730        var isNext=o.id===ROT.nextId;
731        return '<div class="rotrow'+(isNext?" next":"")+'" draggable="true" data-id="'+esc(o.id)+'" title="Drag to reorder, or use the arrows">'+
732          '<span class="rnum">'+(i+1)+'.</span>'+
733          '<span class="rname">'+esc(o.name)+(isNext?'<span class="rnext">next</span>':'')+'</span>'+
734          '<span class="rmove">'+
735            '<button type="button" class="rbtn" data-up="'+esc(o.id)+'" aria-label="Move '+esc(o.name)+' up"'+(i===0?" disabled":"")+'>&uarr;</button>'+
736            '<button type="button" class="rbtn" data-down="'+esc(o.id)+'" aria-label="Move '+esc(o.name)+' down"'+(i===order.length-1?" disabled":"")+'>&darr;</button>'+
737          '</span>'+
738        '</div>';
739      }).join("");
740      el.className="rotation";
741      el.innerHTML='<div class="rothead">Application approval rotation</div><div class="rotsub">Applications auto-assign to the next member in this order. Next up: <b>'+esc(ROT.nextName||"-")+'</b>. Use the arrows to reorder, or drag a name on a computer.</div><div class="rotlist">'+rows+'</div>';
742      // Bound directly, not delegated: this panel is re-rendered wholesale on every
743      // save, so a delegated handler on a parent that also re-renders would be lost.
744      Array.prototype.forEach.call(el.querySelectorAll("[data-up],[data-down]"),function(b){
745        b.addEventListener("click",function(){
746          var id=b.getAttribute("data-up")||b.getAttribute("data-down");
747          var dir=b.hasAttribute("data-up")?-1:1;
748          nudgeRotation(id,dir);
749        });
750      });
751    }
752    function render(){
753      var rows=visible();
754      var list=$("#list");
755      list.innerHTML = rows.length ? rows.map(card).join("") : '<p class="empty">No members match.</p>';
756      renderRotation(); renderCounts(); refreshSel();
756 clampBios();
757    }
758    // Show the "Show more" toggle only on bios that actually overflow the clamped lines.
759    function clampBios(){
760      var ws=document.querySelectorAll("#list .mbiowrap");
761      for(var i=0;i<ws.length;i++){
762        var bx=ws[i].querySelector(".mbio"), btn=ws[i].querySelector(".mbiomore"); if(!bx||!btn) continue;
763        bx.classList.add("clamp"); btn.textContent="Show more";
764        btn.hidden = bx.scrollHeight <= bx.clientHeight + 1;
765      }
766    }
767    // Drag a pill to reorder the rotation; the order saves on drop.
768    (function(){
769      var wrap=$("#rotation"), dragEl=null;
770      wrap.addEventListener("dragstart",function(e){ var r=e.target.closest(".rotrow"); if(!r) return; dragEl=r; r.classList.add("dragging"); e.dataTransfer.effectAllowed="move"; try{ e.dataTransfer.setData("text/plain", r.getAttribute("data-id")); }catch(_){ } });
771      wrap.addEventListener("dragover",function(e){ if(!dragEl) return; var list=e.target.closest(".rotlist"); if(!list) return; e.preventDefault(); e.dataTransfer.dropEffect="move";
772        var over=e.target.closest(".rotrow");
773        if(over&&over!==dragEl){ var rect=over.getBoundingClientRect(); var before=e.clientX < rect.left + rect.width/2; list.insertBefore(dragEl, before?over:over.nextSibling); } });
774      wrap.addEventListener("drop",function(e){ if(dragEl) e.preventDefault(); });
775      wrap.addEventListener("dragend",function(){ if(!dragEl) return; dragEl.classList.remove("dragging"); dragEl=null;
776        var order=[].map.call(wrap.querySelectorAll(".rotrow"),function(r){return r.getAttribute("data-id");});
777        var cur=((ROT&&ROT.order)||[]).map(function(o){return o.id;});
778        if(order.join("|")===cur.join("|")){ renderRotation(); return; } // dropped back where it started
779        post({action:"setRotation",order:order}).then(function(d){ if(d&&d.ok&&d.rotation){ ROT=d.rotation; } renderRotation(); }); });
780    })();
781
782    // Board activity log: fetch on first open, refresh on every open after that.
783    (function(){
784      var box=document.getElementById("actlog"), list=document.getElementById("actlist"); if(!box||!list) return;
785      function fmtAt(ms){ try{ return new Date(ms).toLocaleDateString(undefined,{month:"short",day:"numeric"})+" "+new Date(ms).toLocaleTimeString(undefined,{hour:"numeric",minute:"2-digit"}); }catch(e){ return ""; } }
786      box.addEventListener("toggle",function(){
787        if(!box.open) return;
788        list.innerHTML='<p class="actempty">Loading.</p>';
789        fetch(FN+"/board-members?activity=1&t="+Date.now(),{cache:"no-store"}).then(function(r){return r.json();}).then(function(d){
790          if(!d||!d.ok){ list.innerHTML='<p class="actempty">Could not load the activity log.</p>'; return; }
791          var ev=d.events||[];
792          list.innerHTML = ev.length ? ev.map(function(e){ return '<div class="actrow"><span class="actwhen">'+esc(fmtAt(e.at))+'</span><span>'+esc(e.what)+'</span></div>'; }).join("") : '<p class="actempty">No board activity recorded yet.</p>';
793        }).catch(function(){ list.innerHTML='<p class="actempty">Could not load the activity log.</p>'; });
794      });
795    })();
796    function announceRsvpBootstrap(value){
797      try{ window.dispatchEvent(new CustomEvent("rrcc:board-rsvp-bootstrap",{detail:value||null})); }catch(e){}
798    }
799    function load(){
800      fetch(FN+"/board-members?bootstrap=1",{cache:"no-store"}).then(function(r){return r.json();}).then(function(d){
801        if(!d||!d.ok){ $("#list").innerHTML='<p class="empty">'+((d&&d.error)?esc(d.error):"Could not load board members.")+'</p>'; loadUpdates(); loadMinutes(); announceRsvpBootstrap(null); return; }
802        MEM=d.members||[]; ROT=d.rotation||ROT; render();
803        if(d.bootstrap){
804          applyUpdates(d.bootstrap.updatesError
805            ? {ok:false,error:d.bootstrap.updatesError}
806            : {ok:true,updates:d.bootstrap.updates||[]});
807          applyMinutes(d.bootstrap.minutesError
808            ? {ok:false,error:d.bootstrap.minutesError}
809            : {ok:true,minutes:d.bootstrap.minutes||[],unmatched:d.bootstrap.unmatched||[]});
810          announceRsvpBootstrap(d.bootstrap.rsvps||null);
811        }else{
812          // Safe rolling-deploy fallback if the HTML reaches a browser before the
813          // matching function bundle does.
814          loadUpdates(); loadMinutes(); announceRsvpBootstrap(null);
815        }
816      }).catch(function(){ $("#list").innerHTML='<p class="empty">Could not load (functions only run on the deployed site).</p>'; loadUpdates(); loadMinutes(); announceRsvpBootstrap(null); });
817    }
818
819    // ---- list interactions ----
820    $("#list").addEventListener("click",function(e){
821      var btn=e.target.closest("[data-act]"); if(!btn) return;
822      var id=btn.getAttribute("data-id"), act=btn.getAttribute("data-act");
823      if(act==="biomore"){ var bx=btn.previousElementSibling; var clamped=bx.classList.toggle("clamp"); btn.textContent=clamped?"Show more":"Show less"; return; }
824      var m=mget(id); if(!m) return;
825      if(act==="pick"){ if(btn.checked) SEL[id]=true; else delete SEL[id]; var c=btn.closest(".mcard"); if(c)c.classList.toggle("sel",!!SEL[id]); refreshSel(); return; }
826      if(act==="email"){ window.location.href="mailto:"+m.email; return; }
827      if(act==="sendprofile"){
828        btn.disabled=true; btn.textContent="Sending...";
829        post({action:"sendProfileLink",id:id}).then(function(d){
830          btn.disabled=false; btn.textContent="Send profile link";
831          if(d&&d.ok){ m.profileEmailAt=d.at||Date.now(); m.hasLink=true; toast("Profile link emailed to "+(m.name||d.to)); render(); }
832          else toast((d&&d.error)||"Could not send.");
833        }).catch(function(){ btn.disabled=false; btn.textContent="Send profile link"; toast("Could not send."); });
834        return;
835      }
836      // Reset: the old link stops working and the server emails the new one straight to the member. The link itself never
837      // comes back to this page. An archived member's reset only turns the old link off (that is how a departed member is cut off).
838      if(act==="resetlink"){
839        var who=m.name||"this member";
840        var ask = m.removed ? ("Turn off "+who+"'s dashboard link? Their current link stops working. They are archived, so no new link is sent.")
841          : !m.email ? ("Reset "+who+"'s dashboard link? Their current link stops working. There is no email on file, so no new link can be sent until you add one with Edit.")
842          : ("Reset "+who+"'s dashboard link? Their current link stops working, and the new link is emailed to them at "+m.email+".");
843        if(!confirm(ask)) return;
844        btn.disabled=true;
845        post({action:"resetToken",id:id}).then(function(d){
846          btn.disabled=false;
847          if(d&&d.ok){
848            m.hasLink=true;
849            if(d.emailed){ m.profileEmailAt=d.at||Date.now(); toast("New link emailed to "+(d.to||m.email)+". The old link no longer works."); }
850            else toast("Link reset. The old link no longer works. "+(d.reason||""));
851            render();
852          } else toast((d&&d.error)?d.error:"Could not reset.");
853        }).catch(function(){ btn.disabled=false; toast("Could not reset."); }); return; }
854      if(act==="profile"){ openProfile(m,btn); return; }
855      if(act==="edit"){ openEdit(m); return; }
856      if(act==="archive"){ if(!confirm('Archive "'+m.name+'"? They\'ll be hidden from the active roster and the approval rotation, but kept on file. Restore them anytime with "Show archived".')) return;
857        post({action:"archive",id:id}).then(function(d){ if(d&&d.ok){ m.removed=true; delete SEL[id]; render(); var n=rosterNote(d); if(n) toast("Archived. "+n,9000); } }); return; }
858      if(act==="unarchive"){ post({action:"unarchive",id:id}).then(function(d){ if(d&&d.ok){ m.removed=false; render(); var n=rosterNote(d); if(n) toast("Restored. "+n,9000); } }); return; }
859    });
860
861    $("#selAll").addEventListener("change",function(){ var on=this.checked;
861 visible().forEach(function(m){ if(m.removed) return; if(on)SEL[m.id]=true; else delete SEL[m.id]; }); render(); });
862    ["q","showArchived"].forEach(function(id){ var el=document.getElementById(id); el.addEventListener(el.type==="search"?"input":"change",render); });
863
864    function groupMailto(members, withSubjectBody){
865      if(!members.length) return;
866      var to=members.map(function(m){return encodeURIComponent(m.email);}).filter(Boolean).join(",");
867      var href="mailto:"+to; if(withSubjectBody) href+="?subject="+encodeURIComponent("RRCC Board");
868      window.location.href=href;
869    }
870    $("#emailSel").addEventListener("click",function(){ var s=selectedMembers(); if(!s.length) return; groupMailto(s,false); });
871    $("#linkSel").addEventListener("click",function(){
872      var s=selectedMembers().filter(function(m){return !!m.email;}); if(!s.length){ toast("No selected members with an email on file."); return; }
873      if(!confirm("Email "+s.length+" member"+(s.length===1?"":"s")+" their personal dashboard link?")) return;
874      var okN=0, chain=Promise.resolve();
875      s.forEach(function(m){ chain=chain.then(function(){ return post({action:"sendProfileLink",id:m.id}).then(function(d){ if(d&&d.ok){ okN++; m.profileEmailAt=d.at||Date.now(); } }).catch(function(){}); }); });
876      chain.then(function(){ toast("Sent "+okN+" of "+s.length+" profile link emails."); render(); });
877    });
878
879    // ---- profile: a staff-side view of the member ----
880    // What RRCC holds about the member and the applications handed to them. It deliberately does NOT open the member's own
881    // dashboard: that needs their private link, and a link staff can open is a link staff can use as that member.
882    var profileOv=$("#profileOv"), profileOpener=null;
883    function shortDate(t){ try{ return new Date(t).toLocaleDateString(undefined,{month:"short",day:"numeric",year:"numeric"}); }catch(e){ return ""; } }
884    function openProfile(m,opener){
885      profileOpener=opener||null;
886      var ava = m.hasPhoto ? ('<span class="pf-ava"><img src="'+esc(photoUrl(m))+'" alt=""/></span>') : ('<span class="pf-ava">'+esc(initials(m.name).toUpperCase())+'</span>');
887      var meta=[];
888      if(m.email) meta.push('<a href="mailto:'+esc(m.email)+'">'+esc(m.email)+'</a>'); else meta.push("No email on file");
889      if(m.phone) meta.push(esc(m.phone));
890      if(m.title) meta.push(esc(m.title));
891      var link;
892      if(m.removed) link="Archived. Reset link turns their old link off without sending a new one.";
893      else if(m.hasLink===true) link="They have a private dashboard link."+(m.profileEmailAt?(" Last emailed "+shortDate(m.profileEmailAt)+"."):" It has not been emailed from this page yet.");
894      else if(m.hasLink===false) link="No dashboard link yet. Send profile link makes one and emails it to them.";
895      else link="Their link could not be checked right now.";
896      var apps=Array.isArray(m.reviewApps)?m.reviewApps:[];
897      var appHtml = apps.length ? apps.map(function(a){
898        var score=(a.finalScore!=null)?'<span class="pf-score">'+esc(a.finalScore)+'/20</span>':"";
899        var pill=a.approved?'<span class="pf-status approved">Approved</span>':'<span class="pf-status pending">'+esc(a.status||"Waiting on them")+'</span>';
900        var stamp=(a.approved&&a.boardApprovedAt)?('<div class="pf-meta">Approved '+esc(shortDate(a.boardApprovedAt))+(a.boardApprovedBy?(" by "+esc(a.boardApprovedBy)):"")+'</div>'):"";
901        var note=a.boardNote?('<div class="pf-note">'+esc(a.boardNote)+'</div>'):"";
902        return '<div class="pf-app"><div class="pf-app-top"><span><span class="pf-org">'+esc(a.org||"Application")+'</span>'+score+'</span>'+pill+'</div>'+stamp+note+
903          '<a class="pf-link" href="app-review.html?q='+encodeURIComponent(a.org||"")+'">Open in Application Review</a></div>';
904      }).join("") : '<div class="pf-empty">Nothing assigned to them yet.</div>';
905      $("#pfBody").innerHTML=
906        '<div class="pf-head">'+ava+'<div><h3 class="pf-name" id="pfTitle">'+esc(m.name||"(no name)")+'</h3>'+
907          '<div class="pf-pos">'+esc(m.position||"Board of Directors")+' &middot; Term '+esc(computeTerm(m))+'</div></div></div>'+
908        '<div class="pf-meta">'+meta.join(" &middot;
908 ")+'</div>'+
909        (m.bio?'<div class="pf-bio">'+esc(m.bio)+'</div>':"")+
910        '<div class="pf-sech">Board dashboard link</div>'+
911        '<div class="pf-meta">'+esc(link)+'</div>'+
912        '<div class="pf-meta">Only the member opens their dashboard. Send profile link and Reset link email it to them; the link is never shown here.</div>'+
913        '<div class="pf-sech">Applications assigned to them</div>'+appHtml;
914      profileOv.hidden=false;
915      $("#pfClose").focus();
916    }
917    function closeProfile(){ profileOv.hidden=true; if(profileOpener&&profileOpener.focus) profileOpener.focus(); profileOpener=null; }
918    $("#pfClose").addEventListener("click",closeProfile);
919    profileOv.addEventListener("click",function(e){ if(e.target===profileOv) closeProfile(); });
920    document.addEventListener("keydown",function(e){ if(e.key==="Escape"&&!profileOv.hidden) closeProfile(); });
921
922    // ---- edit / add modal ----
923    var editOv=$("#editOv");
924    function openEdit(m){ editingId=m?m.id:null;
925      $("#editTitle").textContent=m?("Edit "+(m.name||"member")):"Add board member";
926      $("#f_name").value=m?(m.name||""):""; $("#f_position").value=m?(m.position||"Board of Directors"):"Board of Directors";
927      $("#f_term").value=m?computeTerm(m):""; $("#f_start").value=m?(m.start||""):""; $("#f_end").value=m?(m.end||""):""; $("#f_email").value=m?(m.email||""):"";
928      $("#f_email_hint").hidden=!m;   // only when editing someone: a new director has no sign-in address yet
929      editOv.hidden=false; $("#f_name").focus();
930    }
931    function closeEdit(){ editOv.hidden=true; editingId=null; }
932    $("#editCancel").addEventListener("click",closeEdit);
933    editOv.addEventListener("click",function(e){ if(e.target===editOv) closeEdit(); });
934    document.addEventListener("keydown",function(e){ if(e.key==="Escape"&&!editOv.hidden) closeEdit(); });
935    $("#addBtn").addEventListener("click",function(){ openEdit(null); });
936    $("#editSave").addEventListener("click",function(){
937      var payload={ action: editingId?"edit":"add", name:$("#f_name").value.trim(), position:$("#f_position").value, start:$("#f_start").value.trim(), end:$("#f_end").value.trim(), email:$("#f_email").value.trim() };
938      if(editingId) payload.id=editingId;
939      if(!payload.name){ toast("Name is required."); return; }
940      post(payload).then(function(d){ if(d&&d.ok&&d.member){ closeEdit();
941        var i=MEM.findIndex(function(x){return x.id===d.member.id;}); if(i>=0) MEM[i]=Object.assign(MEM[i],d.member); else MEM.push(d.member);
942        render(); var n=rosterNote(d); toast(n?("Saved. "+n):"Saved.", n?9000:0); } else toast((d&&d.error)?d.error:"Could not save."); });
943    });
944
945    var fb=$("#featBtn"); if(fb) fb.addEventListener("click",function(){ var msg=prompt("Describe the feature or change you'd like:"); if(msg){ fetch(FN+"/feature-request",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({staff:"Board Members",message:msg})}).then(function(){alert("Thanks! Sent.");}).catch(function(){}); } });
946
947    wireUpdates();
948    wireMinutes();
949    load();
950  })();
951  
952  /* ---- Meeting RSVPs -------------------------------------------------------
953     One public link (board-rsvp.html) that every member opens; they pick their
954     own name. No token per person, which is what was asked for, so anyone with
955     the link could answer as anyone. Fine for a meeting headcount, and every
956     change is written to the board log with what it replaced. */
957  (function(){
958    var BOX=document.getElementById("rsvpBox");
959    if(!BOX) return;
960    var LABEL={yes:"Yes",no:"No",maybe:"Not sure"};
961    // Own copy: the page's esc() lives in another closure and is not reachable here.
962    function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){ return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]; }); }
963
964    function renderData(d){
965      if(!d||!d.ok){
966        BOX.hidden=false;
967        document.getElementById("rsvpTitle").textContent="Meeting RSVPs";
968        document.getElementById("rsvpWhen").textContent=(d&&d.error)||"The RSVP list is unavailable right now.";
969        document.getElementById("rsvpCounts").innerHTML="";
970        document.getElementById("rsvpList").innerHTML="";
971        return;
972      }
973      BOX.hidden=false;
974      var m=d.meeting;
975      document.getElementById("rsvpTitle").textContent = m&&m.title ? m.title : "Meeting RSVPs";
976      document.getElementById("rsvpWhen").textContent = m
977        ? [m.date, m.time, m.place].filter(Boolean).join(" · ")
978          + (m.from?("  (moved from "+m.from+")"):"")
979          + (d.meetingSet===false?"  [default, not saved yet]":"")
980        : "No meeting set yet. Use Set the meeting to add one.";
981      var rows=d.rows||[];
982      var c={yes:0,no:0,maybe:0,none:0};
983      rows.forEach(function(r){ c[r.answer||"none"]=(c[r.answer||"none"]||0)+1; });
984      document.getElementById("rsvpCounts").innerHTML=
985          '<span class="rsvpchip yes">Yes '+c.yes+'</span>'
986        + '<span class="rsvpchip no">No '+c.no+'</span>'
987        + '<span class="rsvpchip maybe">Not sure '+c.maybe+'</span>'
988        + '<span class="rsvpchip">No reply '+c.none+'</span>';
989      document.getElementById("rsvpList").innerHTML = rows.map(function(r){
990        var a=r.answer||"none";
991        var how = r.answer==="yes" ? (r.how==="virtual"?" · virtual":" · in person") : "";
992        return '<div class="rsvprow"><div><b>'+esc(r.name)+'</b>'
993          + (r.note?'<div class="rsvpnote">'+esc(r.note)+'</div>':'')
994          + '</div><span class="rsvptag '+a+'">'+(LABEL[a]||"No reply")+esc(how)+'</span></div>';
995      }).join("");
996    }
997
998    var fallbackStarted=false;
999    function load(){
1000      fallbackStarted=true;
1001      fetch("/.netlify/functions/board-members?rsvps=1")
1002        .then(function(r){ return r.json(); })
1003        .then(renderData).catch(function(){});
1004    }
1005
1006    window.addEventListener("rrcc:board-rsvp-bootstrap",function(e){
1007      if(e.detail){ renderData(e.detail); }
1008      else if(!fallbackStarted) load();
1009    },{once:true});
1010    // If the main request stalls rather than failing, the RSVP panel still gets a
1011    // bounded independent fallback instead of sitting blank indefinitely.
1012    setTimeout(function(){ if(BOX.hidden&&!fallbackStarted) load(); },6000);
1013
1014    document.getElementById("rsvpCopy").onclick=function(){
1015      var url=location.origin+"/board-rsvp.html";
1016      if(navigator.clipboard) navigator.clipboard.writeText(url);
1017      this.textContent="Link copied";
1018      var b=this; setTimeout(function(){ b.textContent="Copy RSVP link"; },1800);
1019    };
1020
1021    document.getElementById("rsvpSet").onclick=function(){
1022      var date=prompt("Meeting date, as you want it to read:","Tuesday, August 25, 2026");
1023      if(date===null) return;
1024      var time=prompt("Time:","5:30 PM");
1025      if(time===null) return;
1026      var place=prompt("Where:","In person, our complex conference room");
1027      if(place===null) return;
1028      var from=prompt("If this replaces an earlier date, what was it? (blank if not a reschedule)","Tuesday, August 4, 2026");
1029      if(from===null) return;
1030      var why=from?prompt("Why did it move? (one line, shown on the RSVP page)","Weaver needs about another week and a half to finish the audit."):"";
1031      if(why===null) return;
1032      var note=prompt("Anything else to tell them? (optional)","Weaver will present the completed audit at this meeting.");
1033      if(note===null) return;
1034      // MUST be asked. setMeeting rebuilds the whole record, so omitting this field
1035      // here would silently WIPE the join link every time somebody pressed Save.
1036      // Shown only on each member's own dashboard, never on the public RSVP page.
1037      var zoom=prompt("Video link to join (https://... , blank for none). Only board members see this, not the public RSVP page:","https://us06web.zoom.us/j/89739185621");
1038      if(zoom===null) return;
1039      if(!confirm("Save this meeting?\n\n"+date+" at "+time+"\n"+place
1040        +(from?("\nMoved from "+from):"")
1041        +"\n\nThis clears any answers already given, so they are not read as replies to this meeting.")) return;
1042      fetch("/.netlify/functions/board-members",{method:"POST",headers:{"content-type":"application/json"},
1043        body:JSON.stringify({action:"setMeeting",title:"RRCC Board Meeting",date:date,time:time,place:place,from:from,why:why,note:note,zoom:zoom})})
1044        .then(function(r){ return r.json(); })
1045        .then(function(d){ if(d&&d.ok) load(); else alert((d&&d.error)||"Could not save the meeting."); })
1046        .catch(function(){ alert("Could not save the meeting."); });
1047    };
1048
1049  })();
1050</script>
vendor: 1 bytes, line 1050
1050
1051<script>
1052  /* ---- Board items: the board room's staff panel ------------------------------------------------------------------
1053     Staff prepare an item with two questions (the server picks the type and the rule and says it in one sentence),
1054     preview it as a director sees it on a phone and as the email reads, and open it. For the 15 minutes before the
1055     emails go it can be withdrawn without a reason; after that a newer document, the deadline extended once, a
1056     withdrawal with a reason and a comment as RRCC staff are each added to the record. Staff never vote, consent,
1057     sign, confirm the voters or record a result: the route has no such action. Every call goes to board-items, which
1058     auth.js signs with the dashboard session; nothing here ever handles a board member's link. */
1059  (function(){
1060    var BOX=document.getElementById("biBox");
1061    if(!BOX) return;
1062    var FN="/.netlify/functions/board-items";
1063    var TYPES={"application/pdf":1,"application/vnd.openxmlformats-officedocument.wordprocessingml.document":1,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":1,"image/png":1,"image/jpeg":1};
1064    var MAX_BYTES=4*1024*1024, MAX_FILES=10;
1065    var ITEMS=[], PREP={need:"",about:"",docs:[],preview:null,needs:null}, CUR=null, ADDFILE=null, ITEM_EPOCH=0, FILE_EPOCH=0;
1066    function $(id){ return document.getElementById(id); }
1067    function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){ return {"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]; }); }
1068    function post(body){
1069      return fetch(FN,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body||{}),cache:"no-store"})
1070        .then(function(r){ return Promise.resolve(r.json()).catch(function(){ return null; }); })
1071        .then(function(d){ return d&&typeof d==="object"?d:{ok:false,error:"The answer could not be read. Look again before trying once more."}; })
1072        .catch(function(){ return {ok:false,error:"The board items could not be reached. Nothing was changed. Try again in a minute."}; });
1073    }
1074    function msg(id,kind,text){
1075      var el=$(id); if(!el) return;
1076      el.innerHTML=text?('<div class="bi-msg bi-'+kind+'" role="'+(kind==="bad"?"alert":"status")+'">'+esc(text)+'</div>'):"";
1077    }
1078    function lines(arr){ return arr.filter(Boolean).map(function(t){ return '<p>'+esc(t)+'</p>'; }).join(""); }
1079
1080    // ---- the list ----
1081    function load(){
1082      return post({action:"list"}).then(function(d){
1083        if(!d.ok){ $("biList").innerHTML='<div class="rsvpwhen">'+esc(d.error||"Could not load the board items.")+'</div>'; return; }
1084        ITEMS=Array.isArray(d.items)?d.items:[];
1085        var list=ITEMS.slice().sort(function(a,b){ return String(b.id).localeCompare(String(a.id)); });
1086        var h=list.length?list.map(rowHtml).join(""):'<div class="rsvpwhen">No board items yet. Prepare one to put a document, a question or a decision before the board.</div>';
1087        if(d.unreadable) h+='<div class="rsvpwhen">Some items could not be read just now, so this list may be missing them. Try again in a minute.</div>';
1088        $("biList").innerHTML=h;
1089      });
1090    }
1091    var TAG={open:"Open",pending:"Not open yet",closed:"Closed",withdrawn:"Withdrawn"};
1092    function rowHtml(x){
1093      var sub=[x.kind,x.due,x.progress,x.status==="closed"&&x.outcome?("Result: "+x.outcome):""].filter(Boolean).join(" · ");
1094      var acts=(x.canWithdrawEarly?'<button class="btn sm" type="button" data-bi-early="'+esc(x.id)+'">Withdraw before anyone is emailed</button>':"")+
1095        '<button class="btn sm" type="button" data-bi-open="'+esc(x.id)+'">Open</button>';
1096      return '<div class="updrow bi-row" data-id="'+esc(x.id)+'"><div class="updrow-main">'+
1097        '<div class="updrow-t"><span class="bi-id">'+esc(x.id)+'</span> '+esc(x.title)+' <span class="updtag'+(x.status==="open"?"":" draft")+'">'+esc(TAG[x.status]||x.status)+'</span></div>'+
1098        '<div class="rsvpwhen">'+esc(sub)+'</div>'+
1099        (x.status==="closed"||x.status==="withdrawn"?'<div class="rsvpwhen">'+esc(x.statusWords)+'.</div>':"")+
1100        (x.canWithdrawEarly?'<div class="rsvpwhen">Emails go at '+esc(x.emailAtWords)+'. Until then you can withdraw it without a reason.</div>':"")+
1101        '</div><div class="bi-rowacts">'+acts+'</div></div>';
1102    }
1103    $("biList").addEventListener("click",function(e){
1104      var b=e.target.closest("button"); if(!b) return;
1105      if(b.hasAttribute("data-bi-open")) return openItemDlg(b.getAttribute("data-bi-open"));
1106      if(b.hasAttribute("data-bi-early")) return withdrawEarly(b.getAttribute("data-bi-early"),b);
1107    });
1108    function withdrawEarly(id,btn){
1109      var epoch=ITEM_EPOCH;
1110      if(!confirm("Withdraw "+id+" before anyone is emailed? It stays in the record as withdrawn, and no director is emailed.")) return;
1111      if(btn) btn.disabled=true;
1112      post({action:"withdrawEarly",id:id}).then(function(d){
1113        if(btn) btn.disabled=false;
1114        if(!d.ok){ msg("biMsg","bad",d.error||"That did not go through."); return; }
1115        msg("biMsg","ok",id+" is withdrawn. No director was emailed.");
1116        load();
1117        if(CUR===id&&epoch===ITEM_EPOCH) refreshItem();
1118      });
1119    }
1120
1121    // ---- Prepare, Preview, Open ----
1122    function radio(name){ var r=document.querySelector('#biPrepOv input[name="'+name+'"]:checked'); return r?r.value:""; }
1123    function fields(){
1124      var f={need:radio("biNeed"),about:radio("biAbout"),title:$("biTitle").value.trim(),motion:$("biWords").value.trim(),docs:PREP.docs.map(function(d){ return {name:d.name,type:d.type,size:d.size,b64:d.b64}; })};
1125      if(f.about==="budget"){
1126        var pay=radio("biBudgetPay");
1127        if(pay) f.budgetChangesEdPay=pay==="yes";
1128        f.staffPayBasis=$("biStaffPayBasis").value.trim();
1129      }
1130      if(PREP.ratifyItem) f.ratifyItem=PREP.ratifyItem;
1131      if($("biDeadline").value) f.deadlineLocal=$("biDeadline").value;
1132      if(!$("biMeetingFld").hidden&&$("biMeeting").value) f.meetingDate=$("biMeeting").value;
1133      if($("biHelp").value.trim()) f.help=$("biHelp").value.trim();
1134      return f;
1135    }
1136    function stale(){ PREP.rev=(PREP.rev||0)+1; PREP.preview=null; PREP.previewFields=null; $("biPreview").innerHTML=""; $("biSure").hidden=true; $("biPrepActs").hidden=false; }
1137    function openPrep(){
1138      PREP={need:"",about:"",docs:[],preview:null,needs:null};
1139      document.querySelectorAll('#biPrepOv input[type="radio"]').forEach(function(r){ r.checked=false; });
1140      ["biTitle","biWords","biDeadline","biMeeting","biHelp","biFiles","biStaffPayBasis"].forEach(function(id){ $(id).value=""; });
1141      $("biBudgetFields").hidden=true;
1142      $("biRule").textContent="Answer both questions, and the rule for this item shows here in one sentence.";
1143      $("biWordsLabel").textContent="The exact words";
1144      $("biDeadlineLabel").textContent="Deadline, Central time";
1145      $("biMeetingFld").hidden=true;
1146      $("biFilesNow").textContent="PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each.";
1147      $("biPreview").innerHTML=""; msg("biPrepMsg","","");
1148      stale();
1149      $("biPrepOv").hidden=false;
1150      var first=document.querySelector('#biPrepOv input[name="biNeed"]'); if(first) first.focus();
1151    }
1152    function closePrep(){ stale(); $("biPrepOv").hidden=true; $("biSureOpen").disabled=false; }
1153    function ask(){
1154      var need=radio("biNeed"), about=radio("biAbout");
1155      $("biBudgetFields").hidden=about!=="budget";
1156      if(!need||!about) return;
1157      var prep=PREP, rev=PREP.rev;
1158      post({action:"prepare",need:need,about:about}).then(function(d){
1159        if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden) return;
1160        if(!d.ok){ PREP.needs=null; $("biRule").textContent=d.error||"That combination cannot be used."; return; }
1161        PREP.needs=d.needs||{};
1162        $("biRule").textContent="The rule: "+d.sentence+" ("+d.kind+")";
1163        $("biWordsLabel").textContent=PREP.needs.motion?"The exact words of the motion (required)":"The exact words";
1164        $("biDeadlineLabel").textContent=PREP.needs.deadline?"Deadline, Central time (required)":"Deadline, Central time (optional)";
1165        $("biMeetingFld").hidden=!PREP.needs.meetingDate;
1166      });
1167    }
1168    function readFile(file){
1169      return new Promise(function(resolve,reject){
1170        var rd=new FileReader();
1171        rd.onload=function(){ resolve({name:file.name,type:file.type,size:file.size,b64:String(rd.result).split(",")[1]||""}); };
1172        rd.onerror=function(){ reject(new Error("read")); };
1173        rd.readAsDataURL(file);
1174      });
1175    }
1176    function fileProblem(f){
1177      if(!TYPES[f.type]) return f.name+" is not a PDF, Word, Excel, PNG or JPEG file.";
1178      if(f.size>MAX_BYTES) return f.name+" is larger than 4 MB.";
1179      return "";
1180    }
1181    $("biFiles").addEventListener("change",function(){
1182      var list=Array.prototype.slice.call($("biFiles").files||[]);
1183      stale();
1184      var prep=PREP, fileEpoch=++FILE_EPOCH;
1185      if(list.length>MAX_FILES){ PREP.docs=[]; $("biFilesNow").textContent="Attach at most 10 files."; return; }
1186      var bad=list.map(fileProblem).filter(Boolean);
1187      if(bad.length){ PREP.docs=[]; $("biFilesNow").textContent=bad[0]; return; }
1188      Promise.all(list.map(readFile)).then(function(docs){
1189        if(prep!==PREP||fileEpoch!==FILE_EPOCH||$("biPrepOv").hidden) return;
1190        stale();
1191        PREP.docs=docs;
1192        $("biFilesNow").textContent=docs.length?("Ready to attach: "+docs.map(function(d){ return d.name; }).join(", ")):"PDF, Word, Excel, PNG or JPEG. Up to 10 files, 4 MB each.";
1193      }).catch(function(){ if(prep!==PREP||fileEpoch!==FILE_EPOCH||$("biPrepOv").hidden) return; PREP.docs=[]; $("biFilesNow").textContent="A file could not be read. Choose it again."; });
1194    });
1195    $("biPrepOv").addEventListener("change",function(e){
1196      if(e.target&&e.target.type==="radio"){ stale(); ask(); }
1197    });
1198    $("biPrepOv").addEventListener("input",function(e){ if(e.target&&e.target.type!=="radio"&&e.target.type!=="file") stale(); });
1199    function phoneHtml(it){
1200      var me=it.me||{}, help=it.help||{};
1201      var h='<div><p class="bi-cap">How a director sees it</p><div class="bi-phone">';
1202      h+='<div class="bi-ph-head">'+lines(["Your part: "+(me.part||""),it.due,it.progress,it.ifNot])+'</div>';
1203      h+=lines([it.id])+'<p class="bi-ph-title">'+esc(it.title)+'</p>'+lines([it.kind,it.rule&&it.rule.sentence]);
1204      if(it.motion) h+='<p><b>The exact words</b></p><div class="bi-ph-words">'+esc(it.motion)+'</div>';
1205      if(it.docs&&it.docs.length) h+='<p><b>Documents</b></p>'+lines(it.docs.map(function(d){ return d.name; }));
1206      if(help.name) h+=lines(["Questions? Ask "+help.name+(help.role?", "+help.role:"")+"."]);
1207      return h+'</div></div>';
1208    }
1209    function mailHtml(m){
1210      return '<div class="bi-mail"><p class="bi-cap">The email each director gets</p><p><b>Subject:</b> '+esc(m.subject)+'</p><div class="bi-mailtext">'+esc(m.text)+'</div></div>';
1211    }
1212    function preview(){
1213      msg("biPrepMsg","","");
1214      var prep=PREP, rev=PREP.rev, exact=fields();
1215      return post({action:"preview",fields:exact}).then(function(d){
1216        if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden||JSON.stringify(exact)!==JSON.stringify(fields())) return null;
1217        if(!d.ok){ PREP.preview=null; msg("biPrepMsg","bad",d.error||"The preview could not be made."); return null; }
1218        PREP.preview=d;
1219        PREP.previewFields=exact;
1220        $("biPreview").innerHTML='<div class="bi-preview">'+phoneHtml(d.item||{})+mailHtml(d.email||{})+'</div>';
1221        return d;
1222      });
1223    }
1224    $("biPreviewBtn").addEventListener("click",function(){ preview(); });
1225    $("biOpenBtn").addEventListener("click",function(){
1226      var prep=PREP, rev=PREP.rev;
1227      (PREP.preview?Promise.resolve(PREP.preview):preview()).then(function(d){
1228        if(!d||prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden) return;
1229        $("biSureText").textContent=d.openDialog||"Once open, nothing in this item can be changed or deleted.";
1230        $("biPrepActs").hidden=true;
1231        $("biSure").hidden=false;
1232        $("biSureOpen").focus();
1233      });
1234    });
1235    $("biSureBack").addEventListener("click",function(){ $("biSure").hidden=true; $("biPrepActs").hidden=false; });
1236    $("biSureOpen").addEventListener("click",function(){
1237      if(!PREP.preview||!PREP.previewFields||JSON.stringify(PREP.previewFields)!==JSON.stringify(fields())){ stale(); msg("biPrepMsg","bad","Preview the current words before opening this item."); return; }
1238      var prep=PREP, rev=PREP.rev;
1239      var btn=$("biSureOpen"); btn.disabled=true;
1240      post({action:"open",fields:PREP.previewFields}).then(function(d){
1241        if(prep!==PREP||rev!==PREP.rev||$("biPrepOv").hidden){ load(); return; }
1242        btn.disabled=false;
1243        if(!d.ok){ $("biSure").hidden=true; $("biPrepActs").hidden=false; msg("biPrepMsg","bad",d.error||"The item did not open."); return; }
1244        closePrep();
1245        msg("biMsg","ok","Opened "+d.id+". Emails go at "+d.emailAtWords+". Until then you can withdraw it without a reason.");
1246        load();
1247      });
1248    });
1249    $("biPrepCancel").addEventListener("click",closePrep);
1250    $("biNew").addEventListener("click",openPrep);
1251
1252    // ---- the address check (the dry run) ----
1253    $("biCheck").addEventListener("click",function(){
1254      if(!confirm("Open one address-check item for every director in office? Each director uses a personal sign-in link and presses I have read this. It closes in 14 days.")) return;
1255      var btn=$("biCheck"); btn.disabled=true;
1256      post({action:"openAddressCheck"}).then(function(d){
1257        btn.disabled=false;
1258        if(!d.ok){ msg("biMsg","bad",d.error||"The address check did not open."); return; }
1259        msg("biMsg","ok","The address check is open: "+d.id+". Emails go at "+d.emailAtWords+".");
1260        load();
1261      });
1262    });
1263
1264    // ---- one item ----
1265    function itemPost(body){
1266      var id=CUR, epoch=ITEM_EPOCH;
1267      return post(body).then(function(d){ return id!==CUR||epoch!==ITEM_EPOCH?{stale:true}:d; });
1268    }
1269    function openItemDlg(id){
1270      ITEM_EPOCH++;
1271      CUR=id; ADDFILE=null;
1272      $("biItemTitle").textContent=id;
1273      $("biItemBody").innerHTML='<div class="rsvpwhen">Loading.</div>';
1274      $("biItemOv").hidden=false;
1275      refreshItem();
1276    }
1277    function closeItemDlg(){ ITEM_EPOCH++; $("biItemOv").hidden=true; CUR=null; ADDFILE=null; }
1278    function refreshItem(note){
1279      var id=CUR, epoch=ITEM_EPOCH;
1280      return itemPost({action:"item",id:id}).then(function(d){ if(d.stale) return;
1281        if(CUR!==id||epoch!==ITEM_EPOCH) return;
1282        if(!d.ok){ $("biItemBody").innerHTML='<div class="bi-msg bi-bad" role="alert">'+esc(d.error||"The item could not be read.")+'</div>'; return; }
1283        if(d.item) return renderItem(d.item,note);
1284        $("biItemBody").innerHTML='<div class="bi-facts">'+lines(["This item was marked as about the Executive Director, so RRCC staff see only where it stands.","Status: "+d.status+".","Next: "+d.nextStep])+'</div>';
1285      });
1286    }
1287    function commentHtml(c){
1288      var corr=(c.corrections||[]).map(function(x){ return '<p class="bi-ctext"><b>Correction:</b> '+esc(x.text==null?"Hidden.":x.text)+'</p>'; }).join("");
1289      var who='<b>'+esc(c.who)+'</b> &middot; '+esc(new Date(c.at).toLocaleString("en-US",{timeZone:"America/Chicago",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}))+' Central';
1290      if(c.withdrawn) return '<div class="bi-comment"><details><summary>'+esc(c.withdrawn)+'</summary><p>'+who+'</p><p class="bi-ctext">'+esc(c.text==null?"Hidden.":c.text)+'</p>'+corr+'</details></div>';
1291      return '<div class="bi-comment"><p>'+who+'</p><p class="bi-ctext">'+esc(c.text==null?"This comment was hidden by the Board Chair and the Secretary.":c.text)+'</p>'+corr+'</div>';
1292    }
1293    function renderItem(it,note){
1294      var can=it.can||{}, h="";
1295      $("biItemTitle").textContent=it.id+": "+it.title;
1296      h+='<div id="biItemMsg">'+(note?'<div class="bi-msg bi-ok" role="status">'+esc(note)+'</div>':"")+'</div>';
1297      h+='<div class="bi-facts">'+lines([it.statusWords+".",it.kind+".",it.rule&&it.rule.sentence,it.due+".",it.progress,it.status==="closed"&&it.outcome?("Result: "+it.outcome+"."):"","Opened by "+it.openedBy+".",it.help&&it.help.name?("Help contact: "+it.help.name+(it.help.role?", "+it.help.role:"")+"."):"",it.canWithdrawEarly?("Emails go at "+it.emailAtWords+"."):""])+'</div>';
1298      if(it.motion) h+='<h4>The exact words</h4><div class="bi-mailtext">'+esc(it.motion)+'</div>';
1299      var docs=it.docs||[];
1300      if(docs.length) h+='<h4>Documents</h4><ul class="bi-docs">'+docs.map(function(d){ return '<li>'+esc(d.name)+' (version '+esc(d.version)+(d.active?"":", replaced by a newer version")+')'+(d.whatChanged?'. What changed: '+esc(d.whatChanged):"")+'</li>'; }).join("")+'</ul>';
1301      h+='<h4>Discussion</h4>'+((it.comments||[]).length?(it.comments||[]).map(commentHtml).join(""):'<p class="rsvpwhen">No comments yet.</p>');
1302      var t=it.votes&&it.votes.totals;
1303      if(t&&(it.rule&&["majority","straw","uwc","ecWriting"].indexOf(it.rule.kind)>=0)) h+='<p class="rsvpwhen">Answers so far: Yes '+t.yes+', No '+t.no+', Abstain '+t.abstain+', not answered '+t.notVoted+(t.steppedOut?(", stepped out "+t.steppedOut):"")+'.</p>';
1304      if(it.canWithdrawEarly) h+='<div class="modal-actions"><button class="btn" type="button" data-bi-act="early">Withdraw before anyone is emailed</button></div>';
1305      if(can.comment) h+='<h4>Comment as RRCC staff</h4><textarea id="biComment" maxlength="4000" placeholder="Directors see this as a comment from RRCC staff."></textarea><div class="modal-actions"><button class="btn primary" type="button" data-bi-act="comment">Comment as RRCC staff</button></div>';
1306      if(can.addDoc){
1307        var active=docs.filter(function(d){ return d.active; });
1308        h+='<h4>Add a newer document</h4><div class="fld"><label for="biAddFile">The newer file</label><input id="biAddFile" type="file" accept=".pdf,.docx,.xlsx,.png,.jpg,.jpeg"></div>'+
1309          (active.length?'<div class="fld"><label for="biSupersedes">It replaces</label><select id="biSupersedes"><option value="">Nothing, it is an extra document</option>'+active.map(function(d){ return '<option value="'+esc(d.sha256)+'">'+esc(d.name)+'</option>'; }).join("")+'</select></div>':"")+
1310          '<div class="fld"><label for="biWhatChanged">What changed</label><input id="biWhatChanged" type="text" maxlength="300" placeholder="One line directors read before they open it"></div>'+
1311          '<div class="modal-actions"><button class="btn" type="button" data-bi-act="addDoc">Add a newer document</button></div>';
1312      }
1313      if(can.extend) h+='<h4>Extend the deadline</h4><p class="rsvpwhen">You can extend it once. Directors are told the new deadline.</p><div class="fld"><label for="biExtendTo">New deadline, Central time</label><input id="biExtendTo" type="datetime-local"></div><div class="fld"><label for="biExtendWhy">Why</label><input id="biExtendWhy" type="text" maxlength="300"></div><div class="modal-actions"><button class="btn" type="button" data-bi-act="extend">Extend the deadline</button></div>';
1314      if(can.withdraw&&!it.canWithdrawEarly) h+='<h4>Withdraw it</h4><div class="fld"><label for="biWithdrawWhy">Why it is withdrawn</label><input id="biWithdrawWhy" type="text" maxlength="300"></div><div class="modal-actions"><button class="btn" type="button" data-bi-act="withdraw">Withdraw it</button></div>';
1315      if(can.draftMeetingResult&&window.RRCC_BOARD_DECISIONS) h+='<h4>Meeting results</h4>'+window.RRCC_BOARD_DECISIONS.meetingForm(it.meetingMembers,true);
1316      if((it.meetingDrafts||[]).length) h+='<p class="rsvpwhen">'+it.meetingDrafts.length+' meeting draft'+(it.meetingDrafts.length===1?'':'s')+' saved for an officer. A draft is not a board action.</p>';
1317      if(can.offerRatification) h+='<div class="bi-msg bi-warn"><p>No quorum was recorded. This is not a board action. You can prepare a new majority vote to ratify the same motion. It will still need Preview and Open.</p><button class="btn" type="button" data-bi-act="ratify">Prepare ratification vote</button></div>';
1318      h+='<h4>The record</h4><div class="modal-actions"><button class="btn" type="button" data-bi-act="record">Download the record</button><button class="btn" type="button" data-bi-act="verify">Confirm nothing was changed</button></div><div id="biCheckMsg"></div>';
1319      var rec=it.record||[];
1320      if(rec.length) h+='<details class="bi-every"><summary>Every entry ('+rec.length+')</summary><ol class="bi-entries">'+rec.map(function(e){ return '<li>
1320'+esc(e.who)+', '+esc(new Date(e.at).toLocaleString("en-US",{timeZone:"America/Chicago",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}))+' Central: '+esc(e.line)+(e.counted?"":" (not counted)")+'. Proof code '+esc(e.proof)+'</li>'; }).join("")+'</ol></details>';
1321      $("biItemBody").innerHTML=h;
1322      var resultForm=$("biItemBody").querySelector('[data-decision-meeting="staff"]');
1323      if(resultForm) resultForm.addEventListener("submit",function(e){
1324        e.preventDefault();
1325        var fields;
1326        try{ fields=window.RRCC_BOARD_DECISIONS.meetingFields(resultForm); }
1327        catch(err){ msg("biItemMsg","bad",err.message); return; }
1328        var button=resultForm.querySelector('button[type="submit"]'); button.disabled=true;
1329        itemPost(Object.assign({action:"draftMeetingResult",id:it.id},fields)).then(function(d){ if(d.stale) return;
1330          button.disabled=false;
1331          if(CUR!==it.id) return;
1332          done(d,"The draft is saved for the Secretary or Chair to check and record.");
1333        });
1334      });
1335      $("biItemBody").querySelectorAll('[data-attendance]').forEach(function(select){ select.addEventListener('change',function(){ var vote=select.closest('[data-decision-member]').querySelector('[data-vote]'); vote.disabled=select.value!=='present'; if(vote.disabled) vote.value=''; }); });
1336      var ratify=$("biItemBody").querySelector('[data-bi-act="ratify"]');
1337      if(ratify) ratify.addEventListener('click',function(){
1338        closeItemDlg(); openPrep(); PREP.ratifyItem=it.id;
1339        document.querySelector('[name="biNeed"][value="vote"]').checked=true;
1340        document.querySelector('[name="biAbout"][value="'+(["budget","policy","minutes","bylaws"].indexOf(it.type)>=0?it.type:"other")+'"]').checked=true;
1341        $("biTitle").value="Ratify: "+it.title; $("biWords").value=it.motion; ask();
1342      });
1343      ADDFILE=null;
1344      var f=$("biAddFile");
1345      if(f) f.addEventListener("change",function(){
1346        var file=f.files&&f.files[0]; ADDFILE=null;
1347        var epoch=ITEM_EPOCH, fileEpoch=++FILE_EPOCH;
1348        if(!file) return;
1349        var bad=fileProblem(file);
1350        if(bad){ msg("biItemMsg","bad",bad); f.value=""; return; }
1351        readFile(file).then(function(d){ if(d.stale) return; if(epoch===ITEM_EPOCH&&fileEpoch===FILE_EPOCH&&f.isConnected) ADDFILE=d; }).catch(function(){ if(epoch===ITEM_EPOCH&&fileEpoch===FILE_EPOCH&&f.isConnected) msg("biItemMsg","bad","That file could not be read. Choose it again."); });
1352      });
1353    }
1354    function done(d,note){
1355      if(!d.ok){ msg("biItemMsg","bad",d.error||"That did not go through."); return; }
1356      if(d.item) renderItem(d.item,note); else refreshItem(note);
1357      load();
1358    }
1359    $("biItemBody").addEventListener("click",function(e){
1360      var b=e.target.closest("button[data-bi-act]"); if(!b||!CUR) return;
1361      var a=b.getAttribute("data-bi-act"), id=CUR;
1362      if(a==="early") return withdrawEarly(id,b);
1363      if(a==="comment"){
1364        var text=$("biComment").value.trim();
1365        if(!text){ msg("biItemMsg","bad","Write the comment first."); return; }
1366        b.disabled=true;
1367        return itemPost({action:"comment",id:id,text:text}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"Your comment is posted as RRCC staff."); });
1368      }
1369      if(a==="addDoc"){
1370        if(!ADDFILE){ msg("biItemMsg","bad","Choose the newer file first."); return; }
1371        var body={action:"addDoc",id:id,doc:ADDFILE};
1372        var sup=$("biSupersedes"); if(sup&&sup.value) body.supersedes=sup.value;
1373        var wc=$("biWhatChanged").value.trim(); if(wc) body.whatChanged=wc;
1374        b.disabled=true;
1375        return itemPost(body).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The newer document is added."); });
1376      }
1377      if(a==="extend"){
1378        var to=$("biExtendTo").value, why=$("biExtendWhy").value.trim();
1379        if(!to){ msg("biItemMsg","bad","Choose the new deadline."); return; }
1380        if(!why){ msg("biItemMsg","bad","Say why the deadline moves."); return; }
1381        b.disabled=true;
1382        return itemPost({action:"extend",id:id,deadlineLocal:to,reason:why}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The deadline is extended."); });
1383      }
1384      if(a==="withdraw"){
1385        var r=$("biWithdrawWhy").value.trim();
1386        if(!r){ msg("biItemMsg","bad","Say why it is withdrawn."); return; }
1387        if(!confirm("Withdraw "+id+"? Directors can no longer act on it. The item and everything in it stay in the record.")) return;
1388        b.disabled=true;
1389        return itemPost({action:"withdraw",id:id,reason:r}).then(function(d){ if(d.stale) return; b.disabled=false; done(d,"The item is withdrawn."); });
1390      }
1391      if(a==="record"){
1392        b.disabled=true;
1393        return itemPost({action:"record",id:id}).then(function(d){ if(d.stale) return;
1394          b.disabled=false;
1395          if(!d.ok||!d.b64){ msg("biCheckMsg","bad",d.error||"The record could not be made."); return; }
1396          var bin=atob(d.b64), u8=new Uint8Array(bin.length);
1397          for(var i=0;i<bin.length;i++) u8[i]=bin.charCodeAt(i);
1398          var a2=document.createElement("a");
1399          a2.href=URL.createObjectURL(new Blob([u8],{type:"application/pdf"}));
1400          a2.download=d.fileName||("rrcc-board-record-"+id+".pdf");
1401          document.body.appendChild(a2); a2.click();
1402          setTimeout(function(){ try{ URL.revokeObjectURL(a2.href); }catch(_){ } a2.remove(); },1000);
1403          msg("biCheckMsg","ok","The record downloaded. Every page carries check code "+d.checkCode+".");
1404        });
1405      }
1406      if(a==="verify"){
1407        b.disabled=true;
1408        return itemPost({action:"verify",id:id}).then(function(d){ if(d.stale) return;
1409          b.disabled=false;
1410          if(!d.ok||!d.check){ msg("biCheckMsg","bad",d.error||"The check could not be done just now."); return; }
1411          msg("biCheckMsg",d.check.ok?"ok":"bad",d.check.words);
1412        });
1413      }
1414    });
1415    $("biItemClose").addEventListener("click",closeItemDlg);
1416    ["biPrepOv","biItemOv"].forEach(function(id){
1417      $(id).addEventListener("click",function(e){ if(e.target===$(id)){ if(id==="biPrepOv") closePrep(); else closeItemDlg(); } });
1418    });
1419    document.addEventListener("keydown",function(e){
1420      if(e.key!=="Escape") return;
1421      if(!$("biItemOv").hidden) closeItemDlg();
1422      else if(!$("biPrepOv").hidden) closePrep();
1423    });
1424    load();
1425  })();
1426</script>
1426
1427</body>
1428</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.