PageSourceSearch

https://drnotesapp.com/demo_webapp

html drnotesapp.com collected 2026-10-02 00:48:27 UTC 152,582 bytes, 1,147 lines download raw bytes

1<!doctype html><html lang="en" data-theme="light"><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><title>Interactive Demo – Dr. Notes for Psychiatrists</title>
2<meta name="description" content="Try Dr. Notes for Psychiatrists in your browser: walk through a psychiatric evaluation with safety, symptoms, history, MSE, PHQ-9 and GAD-7 scales, formulation and plan, using sample patients.">
3<link rel="canonical" href="https://drnotesapp.com/demo_webapp">
4<meta property="og:type" content="website">
5<meta property="og:site_name" content="Dr. Notes">
6<meta property="og:url" content="https://drnotesapp.com/demo_webapp">
7<meta property="og:title" content="Interactive Demo – Dr. Notes for Psychiatrists">
8<meta property="og:description" content="Walk through a psychiatric evaluation in the Dr. Notes for Psychiatrists app, right in your browser.">
9<meta property="og:image" content="https://drnotesapp.com/assets/images/Psychiatry-hero.jpeg">
10<meta name="twitter:card" content="summary_large_image">
11<meta name="twitter:title" content="Interactive Demo – Dr. Notes for Psychiatrists">
12<meta name="twitter:description" content="Walk through a psychiatric evaluation in the Dr. Notes for Psychiatrists app, right in your browser.">
13<meta name="twitter:image" content="https://drnotesapp.com/assets/images/Psychiatry-hero.jpeg">
14<link rel="icon" type="image/png" sizes="32x32" href="/assets/images/favicon-32.png">
15  <link rel="icon" type="image/png" sizes="192x192" href="/assets/images/favicon-192.png">
16  <link rel="apple-touch-icon" href="/assets/images/apple-touch-icon.png"><link rel="preload" as="image" href="/assets/images/drnotes-demo-splash-logo.png" fetchpriority="high"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
17<!-- Interactive web mockup of the Dr. Notes for Psychiatrists iOS app.
18     Self-contained: no network requests, no data collection. Embed with
19     <iframe src="demo/index.html" width="100%" height="900" style="border:0"></iframe>
20     The app always sits in a fixed 393x852 iPhone frame; if the embedding frame is smaller, the phone scales down to fit. -->
21<style>
22:root{
23  --bg:#FAF9F7;--surface:#FFFFFF;--surface2:#F1EFEB;--text:#20272B;--text2:#6F777C;--text3:#9EA5A9;
24  --accent:#3F6F87;--on-accent:#FFFFFF;--accent-subtle:#EBF1F5;
25  --danger:#A34C42;--warning:#996B2B;--success:#3F6B54;--info:#41667F;
26  --danger-s:#F7E9E6;--warning-s:#F6EEE0;--success-s:#E9F0EB;--divider:#E5E3DF;--recording:#B2564B;
27  --h0:#4F5D95;--h1:#A8546B;--h2:#A07438;--h3:#3F7A63;--h4:#3D6E96;
28  --stage:#dfe7ee;--bezel:#1c1c1e;
29  --font:-apple-system,BlinkMacSystemFont,"SF Pro Text","Segoe UI",Roboto,Helvetica,Arial,sans-serif;
30}
31@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
32  color-scheme:dark;
33  --bg:#121211;--surface:#1B1B1A;--surface2:#252523;--text:#EDECEA;--text2:#A2A9AE;--text3:#757C81;
34  --accent:#8AB3C8;--on-accent:#0E1013;--accent-subtle:#1F2B33;
35  --danger:#D98D85;--warning:#CBA36A;--success:#86B69C;--info:#88A8C0;
36  --danger-s:#33211F;--warning-s:#322818;--success-s:#1C2A23;--divider:#2C2C2A;--recording:#D98D85;
37  --h0:#94A0D4;--h1:#D8909F;--h2:#D6AC72;--h3:#84BFA5;--h4:#86AECF;--stage:#0d1114;--bezel:#2a2a2c;}}
38:root[data-theme="dark"]{
39  color-scheme:dark;
40  --bg:#121211;--surface:#1B1B1A;--surface2:#252523;--text:#EDECEA;--text2:#A2A9AE;--text3:#757C81;
41  --accent:#8AB3C8;--on-accent:#0E1013;--accent-subtle:#1F2B33;
42  --danger:#D98D85;--warning:#CBA36A;--success:#86B69C;--info:#88A8C0;
43  --danger-s:#33211F;--warning-s:#322818;--success-s:#1C2A23;--divider:#2C2C2A;--recording:#D98D85;
44  --h0:#94A0D4;--h1:#D8909F;--h2:#D6AC72;--h3:#84BFA5;--h4:#86AECF;--stage:#0d1114;--bezel:#2a2a2c;}
45*{box-sizing:border-box}
46html,body{min-height:100%;margin:0}
47body{font-family:var(--font);background:var(--stage);color:var(--text);font-size:16px;-webkit-font-smoothing:antialiased}
48button{font-family:inherit;font-size:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-align:left}
49button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
50input,select,textarea{font-family:inherit;font-size:16px;color:var(--text)}
51svg.i{width:1em;height:1em;display:inline-block;vertical-align:-0.15em;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex:none}
52svg.i.f{fill:currentColor;stroke:none}
53
54/* stage + phone */
55.stage{--demo-phone-gap:32px;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:16px;position:relative;background:radial-gradient(circle at 20% 10%,color-mix(in srgb,var(--h4) 22%,var(--stage)),var(--stage) 60%)}
56.demo-back{position:absolute;top:24px;left:24px;z-index:10;display:inline-flex;align-items:center;gap:8px;min-height:42px;padding:0 15px;border:1px solid rgba(63,111,135,.2);border-radius:999px;background:rgba(255,255,255,.9);box-shadow:0 8px 20px rgba(7,27,69,.08);color:var(--accent);font-size:14px;font-weight:600;text-decoration:none;transition:transform .16s ease,background .16s ease}
57.demo-back:hover{background:#fff;transform:translateY(-1px)}
58.phone{width:min(403px,calc(100vw - 32px),calc((100dvh - var(--demo-phone-gap)) * 403 / 862));aspect-ratio:403 / 862;border-radius:56px;background:var(--bezel);padding:5px;box-shadow:0 30px 60px rgba(0,0,0,.28),0 0 0 2px rgba(255,255,255,.06) inset;flex:none}
59.screen{width:100%;height:100%;border-radius:51px;overflow:hidden;background:var(--bg);position:relative;display:flex;flex-direction:column;isolation:isolate}
60.sbar{height:50px;flex:none;display:flex;align-items:flex-end;justify-content:space-between;padding:0 30px 4px;font-size:15px;font-weight:600}
61.sbar .isl{position:absolute;left:50%;top:12px;transform:translateX(-50%);width:120px;height:34px;border-radius:20px;background:#000}
62.sbar .r{display:flex;gap:5px;align-items:center;font-size:13px}
63.stage{overflow:hidden}
64
65@media (max-width:430px),(max-height:900px){
66  .stage{--demo-phone-gap:76px;padding-top:60px}
67  .demo-back{top:10px;left:12px;min-height:36px;padding:0 12px;font-size:13px}
68  .phone{padding:clamp(4px,1.3vw,5px);border-radius:clamp(38px,12vw,56px)}
69  .screen{border-radius:clamp(34px,10.8vw,51px)}
70}
71
72/* header */
73.hdr{flex:none;padding:8px 18px 10px;position:relative;background:var(--bg)}
74.hdr::before{content:none}
75.hdr>*{position:relative}
76.hrow{height:44px;display:flex;align-items:center;gap:8px}
77.hrow .sp{flex:1}
78.disc{width:44px;height:44px;border-radius:50%;background:var(--surface);border:1px solid var(--divider);display:flex;align-items:center;justify-content:center;color:var(--accent);font-size:19px;flex:none}
79.disc.sm{width:34px;height:34px;font-size:17px}
80.htitle{font-size:28px;font-weight:600;letter-spacing:-.4px;margin-top:6px;line-height:1.15;text-wrap:balance}
81.hsub{font-size:15px;color:var(--text2);margin-top:3px}
82.hdr .mark{width:44px;height:44px;display:flex;align-items:center}
83.hdr .mark img{width:40px;height:40px}
84.ctitle{font-size:17px;font-weight:600}
85
86/* The home screen has a brand mark instead of navigation controls. Keep that
87   row deliberately compact so the greeting reads as the screen header—not as
88   a second, oversized toolbar. */
89.hdr:has(.mark){padding-top:6px;padding-bottom:8px}
90.hdr:has(.mark) .hrow{height:36px}
91.hdr:has(.mark) .mark{width:36px;height:36px}
92.hdr:has(.mark) .mark img{width:32px;height:32px}
93.hdr:has(.mark) .htitle{margin-top:2px;font-size:26px;line-height:1.12}
94
95/* content */
96.content{flex:1;overflow-y:auto;overflow-x:hidden;padding:4px 18px 112px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin;contain:layout paint;overscroll-behavior:contain;scroll-behavior:auto;-webkit-overflow-scrolling:touch;touch-action:pan-y;will-change:scroll-position}
97.content.nobar{padding-bottom:24px}
98/* Some evaluation views deliberately use the compact content variant, but the
99   global tab bar still overlays them. Reserve its footprint so the final
100   action is scrollable above the navigation controls. */
101.screen:has(.tabbar) .content.nobar{padding-bottom:112px}
102.entry-actions{flex:none;padding:10px 18px 16px;background:var(--bg);border-top:1px solid var(--divider);box-shadow:0 -8px 18px color-mix(in srgb,var(--bg) 75%,transparent)}
103.card{background:var(--surface);border:1px solid var(--divider);border-radius:10px;padding:16px;display:flex;flex-direction:column;gap:12px}
104.card.xl{border-radius:16px}
105.group{background:var(--surface);border:1px solid var(--divider);border-radius:10px;overflow:hidden}
106.group>*+*{border-top:1px solid var(--divider)}
107.row{display:flex;align-items:center;gap:12px;padding:12px 16px;width:100%}
108.row:active{background:var(--surface2)}
109.row .main{flex:1;min-width:0}
110.row .chev{color:var(--text3);font-size:14px}
111.t1{font-size:16px;font-weight:600;line-height:1.3}
112.t1m{font-size:16px;font-weight:500;line-height:1.3}
113.cap{font-size:13px;color:var(--text2);line-height:1.35}
114.cap3{font-size:13px;color:var(--text3)}
115.callout{font-size:15px;color:var(--text2);line-height:1.4}
116.ov{font-size:12px;font-weight:600;letter-spacing:.6px;text-transform:uppercase;color:var(--text3)}
117.ov.ac{color:var(--accent)}
118.sec{display:flex;align-items:center;gap:8px}
119.sec .st{font-size:20px;font-weight:600;line-height:1.2}
120.sec .sc{font-size:13px;color:var(--text2)}
121.sec .ico{color:var(--text2);font-size:16px}
122.sec .sp{flex:1}
123.mono{font-variant-numeric:tabular-nums}
124.ellip{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
125
126.btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;min-height:52px;border-radius:8px;padding:0 20px;font-size:16px;font-weight:500}
127.btn.p{background:var(--accent);color:var(--on-accent)}
128.btn.p:disabled{opacity:.32;cursor:default}
129.btn.s{background:var(--surface);color:var(--accent);
129border:1px solid var(--divider);min-height:50px}
130.btn.d{color:var(--danger)}
131.btn:active{opacity:.8}
132.quiet{color:var(--accent);font-size:14px;font-weight:500;padding:4px 0}
133.quiet.dn{color:var(--danger)}
134.pill{display:inline-flex;align-items:center;gap:6px;background:var(--accent-subtle);color:var(--accent);border-radius:10px;padding:8px 12px;font-size:14px;font-weight:500}
135.tag{display:inline-block;font-size:13px;font-weight:600;padding:3px 8px;border-radius:6px;background:var(--surface2);color:var(--text2);white-space:nowrap}
136.tag.ac{background:var(--accent-subtle);color:var(--accent)}
137.tag.ok{background:var(--success-s);color:var(--success)}
138.tag.wn{background:var(--warning-s);color:var(--warning)}
139.tag.dn{background:var(--danger-s);color:var(--danger)}
140.tag.cap{border-radius:999px}
141.tile{display:flex;align-items:center;justify-content:center;border-radius:8px;flex:none;color:var(--tint,var(--accent));background:color-mix(in srgb,var(--tint,var(--accent)) 12%,transparent)}
142.tile.fill{background:var(--accent);color:var(--on-accent)}
143.av{border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;flex:none;color:var(--hue);background:color-mix(in srgb,var(--hue) 14%,transparent);letter-spacing:.2px}
144.empty{text-align:center;padding:20px 16px;display:flex;flex-direction:column;align-items:center;gap:8px}
145.empty .t{font-size:18px;font-weight:600}
146.search{display:flex;align-items:center;gap:8px;background:var(--surface);border:1px solid var(--divider);border-radius:8px;padding:10px 12px;color:var(--text3)}
147.search input{flex:1;border:0;background:none;padding:0;min-width:0}
148.search input::placeholder{color:var(--text3)}
149
150/* timeline row */
151.tl{display:flex;align-items:center;gap:8px;padding:11px 10px 11px 8px;border-radius:10px;background:color-mix(in srgb,var(--hue) 7%,transparent);width:100%;position:relative}
152.tl .tm{width:46px;flex:none;color:var(--hue)}
153.tl .tm b{display:block;font-size:15px;font-weight:600;font-variant-numeric:tabular-nums;line-height:1.1}
154.tl .tm span{font-size:12px;opacity:.75}
155.tl .mk{width:12px;flex:none;position:relative;align-self:stretch;display:flex;align-items:center;justify-content:center}
156.tl .mk::before{content:"";position:absolute;top:-12px;bottom:-12px;left:5px;width:1.5px;background:color-mix(in srgb,var(--hue) 28%,transparent)}
157.tl:first-child .mk::before{top:50%}.tl:last-child .mk::before{bottom:50%}
158.tl .mk::after{content:"";width:7px;height:7px;border-radius:50%;background:var(--hue);position:relative}
159.tl .av{background:color-mix(in srgb,var(--hue) 16%,transparent)}
160.tl .main{flex:1;min-width:0}
161.tl .main .callout{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
162.tl .pl{font-size:12px;font-weight:600;padding:3px 8px;border-radius:999px;background:color-mix(in srgb,var(--hue) 12%,transparent);color:var(--hue);white-space:nowrap}
163.tl .pl.wn{background:var(--warning-s);color:var(--warning)}.tl .pl.ok{background:var(--success-s);color:var(--success)}.tl .pl.nt{background:var(--surface2);color:var(--text2)}
164.tlist{display:flex;flex-direction:column;gap:8px}
165
166/* counters + action cards */
167.counters{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;padding:8px}
168.ct{display:flex;align-items:center;gap:6px;padding:8px 6px;border-radius:8px;background:color-mix(in srgb,var(--tint) 8%,transparent)}
169.ct .ico{color:var(--tint);font-size:20px}
170.ct b{display:block;font-size:17px;font-weight:600;line-height:1.1}
171.ct span{font-size:11px;color:var(--text2);white-space:nowrap}
172.acts{display:grid;grid-template-columns:1fr 1fr;gap:12px}
173.act{background:var(--surface);border:1px solid var(--divider);border-radius:16px;padding:16px;display:flex;flex-direction:column;gap:10px;width:100%}
174.act .t{display:flex;align-items:center;justify-content:space-between;font-size:17px;font-weight:600;margin-top:4px}
175.act .t .i{color:var(--accent)}
176
177/* patient cards */
178.pc{background:var(--surface);border:1px solid var(--divider);border-radius:24px;padding:16px;box-shadow:0 4px 10px rgba(0,0,0,.06);display:flex;flex-direction:column;gap:14px}
179.pc .top{display:flex;align-items:center;gap:12px;width:100%;text-align:left}
180.pc .nm{font-size:17px;font-weight:600}
181.pc .age{display:inline-block;margin-top:6px;background:var(--surface2);border-radius:999px;padding:4px 10px;font-size:14px;font-weight:500}
182.pc .lv{text-align:right;margin-left:auto}
183.pc .lv .l{font-size:13px;color:var(--text2)}.pc .lv .v{font-weight:600;font-size:16px}
184.pc .bt{display:grid;grid-template-columns:1fr 1fr;gap:10px}
185.pc .bt button{height:46px;border-radius:999px;background:var(--accent-subtle);color:var(--accent);font-weight:500;display:flex;align-items:center;justify-content:center;gap:8px;font-size:15px}
186
187.kv{display:flex;justify-content:space-between;gap:16px;padding:11px 0;
187border-top:1px solid var(--divider);font-size:15px}
188.kv:first-child{border-top:0;padding-top:0}.kv:last-child{padding-bottom:0}
189.kv .k{color:var(--text2);flex:none}.kv .v{font-weight:600;text-align:right}
190.kv .v.dn{color:var(--danger)}.kv .v.wn{color:var(--warning)}.kv .v.ok{color:var(--success)}
191.kvs .kv{padding:10px 0}
192.lbl{font-size:13px;color:var(--text2)}
193.prose{font-size:15px;line-height:1.45}
194
195/* history timeline */
196.hist{display:flex;flex-direction:column;gap:12px;position:relative;padding-left:26px}
197.hist::before{content:"";position:absolute;left:6px;top:12px;bottom:12px;width:1px;background:var(--divider)}
198.hist .ev{position:relative;width:100%;text-align:left}
199.hist .ev::before{content:"";position:absolute;left:-26px;top:18px;width:13px;height:13px;border-radius:50%;background:var(--surface2);border:2px solid var(--accent)}
200.hist .ev.latest::before{background:var(--accent)}
201.hist .ev .card{gap:6px}
202.hist .ev.latest .card{border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
203
204/* forms */
205.fs{display:flex;flex-direction:column;gap:12px}
206.fl{font-size:14px;font-weight:500;color:var(--text2);display:block;margin-bottom:6px}
207.fl .req{color:var(--danger);opacity:.8}
208.well{height:52px;background:var(--surface2);border:1px solid var(--divider);border-radius:8px;padding:0 12px;width:100%;display:flex;align-items:center;gap:8px}
209.well input,.well select{flex:1;border:0;background:none;min-width:0;height:100%;padding:0;appearance:none;-webkit-appearance:none}
210.well input::placeholder{color:var(--text3)}
211.well .i{color:var(--text3);font-size:14px}
212.well.ta{height:auto;padding:10px 12px}
213.well textarea{width:100%;border:0;background:none;resize:none;padding:0;min-height:60px;line-height:1.4}
214.two{display:grid;grid-template-columns:1fr 1fr;gap:10px}
215.seg{display:flex;gap:6px}
216.seg button{min-width:50px;height:36px;border-radius:6px;background:var(--surface2);color:var(--text2);font-size:14px;font-weight:500;padding:0 10px;display:flex;align-items:center;justify-content:center}
217.seg button.on{background:var(--accent-subtle);color:var(--accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent) inset}
218.toggle{width:50px;height:30px;border-radius:999px;background:var(--divider);position:relative;flex:none;transition:background .18s}
219.toggle::after{content:"";position:absolute;top:3px;left:3px;width:24px;height:24px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:left .18s}
220.toggle.on{background:var(--accent)}.toggle.on::after{left:23px}
221.trow{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0}
222
223/* evaluation workspace */
224.recbar{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--divider);border-radius:12px;padding:12px 14px}
225.recbar .ic{width:32px;height:32px;border-radius:50%;background:color-mix(in srgb,var(--accent) 13%,transparent);color:var(--accent);display:flex;align-items:center;justify-content:center;font-size:16px;flex:none}
226.recbar .ic.live{animation:pulse 1.6s ease-in-out infinite}
227.recbar .ic.wn{background:var(--warning-s);color:var(--warning)}
228@keyframes pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--recording) 35%,transparent)}50%{box-shadow:0 0 0 7px transparent}}
229.recbar .main{flex:1}
230.recbar .st{font-weight:600;font-size:16px}
231.recbar .tm{font-size:13px;color:var(--text2);font-variant-numeric:tabular-nums}
232.recbar .bts{display:flex;gap:12px;flex:none}
233.recbar .bts button{font-weight:600;font-size:15px}
234.recbar .bts .wn{color:var(--warning)}
235.mini{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--divider);border-radius:12px;padding:12px 14px}
236.eval-header{padding:6px 18px 8px}
237.eval-header .hrow{height:44px;gap:12px}
238.eval-header-title{font-size:20px;font-weight:600;letter-spacing:-.2px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
239.tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--divider);margin:0 -18px;padding:0 12px;background:var(--bg);min-height:48px}
240.tabs .sc{flex:1;display:flex;overflow-x:auto;scrollbar-width:none;gap:0}
241.tabs .sc::-webkit-scrollbar{display:none}
242.tabs .tb{flex:none;padding:13px 13px 10px;font-size:15px;font-weight:500;color:var(--text2);border-bottom:3px solid transparent;display:flex;gap:6px;align-items:center;white-space:nowrap}
243.tabs .tb.on{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
244.tabs .tb .g{font-size:13px}
245.tabs .tb .g.ok{color:var(--success)}.tabs .tb .g.md{color:var(--warning)}.tabs .tb .g.no{color:var(--text3)}
246.tabs .ar{width:28px;color:var(--text3);font-size:16px;padding:6px;text-align:center}
247.shead .st{font-size:22px;font-weight:600}
248.shead .sc{font-size:13px;color:var(--text2);margin-top:2px}
249.pkg{background:var(--surface2);
249border:1px solid var(--divider);border-radius:14px;overflow:hidden}
250.pkg>*+*{border-top:1px solid var(--divider)}
251.pk{display:flex;align-items:center;gap:10px;padding:12px 14px;min-height:60px;width:100%}
252.pk .main{flex:1;min-width:0}
253.pk .l{font-size:13px;font-weight:600;color:var(--text2)}
254.pk .v{font-size:17px;font-weight:600;margin-top:2px}
255.pk .v.no{color:var(--text3);font-weight:400}
256.pk .ud{color:var(--text3);font-size:14px}
257.fld{display:flex;flex-direction:column;gap:8px}
258.fld .lh{display:flex;justify-content:space-between;align-items:center}
259.fld .lh .l{font-size:13px;font-weight:600;color:var(--text2)}
260.chips{display:flex;flex-wrap:wrap;gap:8px}
261.chips.eq{display:grid;grid-template-columns:repeat(3,1fr)}
262.chip{min-height:46px;border-radius:8px;background:var(--surface2);color:var(--text);padding:8px 12px;font-size:15px;font-weight:500;display:flex;align-items:center;justify-content:center;gap:6px;text-align:center}
263.chip.on{background:var(--accent-subtle);color:var(--accent);box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent) inset}
264.chip.sm{min-height:42px;font-size:14px}
265.chips.wrap .chip{flex:1 1 auto}
266.narr textarea{width:100%;border:1px solid var(--divider);border-radius:14px;background:var(--surface2);padding:12px;resize:vertical;line-height:1.4;font-size:15px}
267.narr textarea::placeholder{color:var(--text3)}
268.ideation{background:color-mix(in srgb,var(--warning) 8%,transparent);border:1px solid var(--warning);border-radius:16px;padding:14px;display:flex;flex-direction:column;gap:14px}
269.ideation .hd{display:flex;align-items:center;gap:8px;color:var(--warning);font-size:12px;font-weight:600;letter-spacing:.6px}
270.ptitle{font-size:19px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;margin-top:8px}
271.disc-row{display:flex;justify-content:space-between;align-items:center;padding:12px 0;font-weight:600;font-size:16px;width:100%}
272.disc-row .i{color:var(--text2)}
273.coll{background:var(--surface);border:1px solid var(--divider);border-radius:12px;padding:0 14px}
274.coll .body{padding:0 0 14px;display:flex;flex-direction:column;gap:14px}
275.navbtns{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:8px}
276.navbtns.one{grid-template-columns:1fr}
277.scard{background:var(--surface2);border:1px solid var(--divider);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:4px;width:100%;text-align:left}
278.scard.done{border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
279.scard .hd{display:flex;justify-content:space-between;align-items:center;font-size:18px;font-weight:700}
280.scard .hd .i{color:var(--text3);font-size:15px}
281.scard .big{font-size:24px;font-weight:700;margin-top:4px}
282.scard .sev{font-weight:600;font-size:15px}
283.scard .lnk{color:var(--accent);font-weight:500}
284.prev{background:var(--surface2);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
285.prev .ov{color:var(--text2)}
286.prev .kv{padding:5px 0;border:0;font-size:16px}
287.prev .kv .k{color:var(--text)}
288.dchips{display:flex;flex-wrap:wrap;gap:8px}
289.dchips span{background:var(--accent-subtle);color:var(--accent);padding:8px 12px;border-radius:8px;font-size:14px;font-weight:500;flex:1;text-align:center}
290.risk{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:10px;font-weight:600;font-size:15px}
291.risk.wn{background:var(--warning-s);color:var(--warning)}.risk.dn{background:var(--danger-s);color:var(--danger)}.risk.ok{background:var(--success-s);color:var(--success)}.risk.nt{background:var(--surface2);color:var(--text2)}
292.scstrip{display:flex;align-items:center;gap:10px;background:var(--surface2);border-radius:8px;padding:10px 12px;font-size:14px}
293.scstrip b{font-weight:700}.scstrip .sp{flex:1;text-align:right;color:var(--text2)}
294.rcard .hd{display:flex;justify-content:space-between;align-items:center}
295.rcard .hd .t{font-size:19px;font-weight:700}
296.cmp{display:grid;grid-template-columns:1fr 1fr;gap:10px}
297.cmp .c{background:var(--surface2);border-radius:8px;padding:10px 12px}
298.cmp .c .l{font-size:12px;color:var(--text2)}
299.cmp .c .v{font-size:14px;margin-top:3px;color:var(--text2)}.cmp .c .v b{color:var(--text)}
300.transcript{background:var(--surface2);border-radius:12px;padding:14px;font-size:15px;line-height:1.5;white-space:pre-wrap}
301.transcript textarea{width:100%;min-height:220px;border:0;background:none;resize:vertical;font-size:15px;line-height:1.5}
302.player{display:flex;align-items:center;gap:12px}
303.player .pb{width:34px;height:34px;border-radius:50%;background:var(--accent);color:var(--on-accent);display:flex;align-items:center;justify-content:center;font-size:15px;flex:none}
304.slider{height:4px;border-radius:2px;
304background:var(--divider);position:relative;flex:1}
305.slider::after{content:"";position:absolute;left:0;top:0;bottom:0;width:var(--p,0%);background:var(--accent);border-radius:2px}
306.overlay-audio{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--text2);font-variant-numeric:tabular-nums}
307
308/* scales questionnaire */
309.prog{display:flex;flex-direction:column;gap:8px}
310.prog .t{font-size:15px;font-weight:600;color:var(--text2)}
311.prog .bar{height:6px;border-radius:3px;background:var(--divider);overflow:hidden}
312.prog .bar i{display:block;height:100%;background:var(--accent);border-radius:3px;transition:width .2s}
313.qcard{background:var(--surface);border:.5px solid var(--divider);border-radius:10px;padding:16px;display:flex;flex-direction:column;gap:8px}
314.qcard .tf{font-size:12px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(--text2)}
315.qcard .q{font-size:20px;font-weight:600;line-height:1.3}
316.opts{display:flex;flex-direction:column;gap:10px}
317.opt{min-height:52px;padding:0 16px;border-radius:14px;background:var(--surface);border:1px solid var(--divider);display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%}
318.opt .i{font-size:20px;color:var(--text3)}
319.opt.on{background:color-mix(in srgb,var(--accent) 10%,transparent);border-color:color-mix(in srgb,var(--accent) 35%,transparent)}
320.opt.on .i{color:var(--accent)}
321.tbtn{height:52px;border-radius:18px;width:100%;display:flex;align-items:center;justify-content:center;font-size:15.5px;font-weight:600}
322.tbtn.p{background:var(--accent);color:var(--on-accent)}.tbtn.p:disabled{opacity:.35;cursor:default}
323.tbtn.s{background:var(--surface);border:1px solid rgba(0,0,0,.065);color:var(--text)}
324.tbtn:active{transform:scale(.97)}
325.resp{background:var(--surface);border-radius:12px;padding:12px;display:flex;flex-direction:column;gap:3px}
326.resp .q{font-size:15px;font-weight:500;line-height:1.35}.resp .a{font-size:13px;color:var(--text2)}
327.score{font-size:34px;font-weight:700}
328.tool{display:flex;align-items:center;gap:14px;width:100%;text-align:left}
329.tool .tile{width:56px;height:56px;font-size:28px}
330.tool .tt{font-size:19px;font-weight:600}
331.tool .mc{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
332.tool .mc span{font-size:14px;font-weight:500;padding:5px 8px;border-radius:999px;background:color-mix(in srgb,var(--tint,var(--accent)) 10%,transparent);color:var(--tint,var(--accent))}
333
334/* tab bar */
335.tabbar{position:absolute;left:18px;right:18px;bottom:8px;display:flex;gap:2px;padding:8px 6px;border-radius:28px;background:color-mix(in srgb,var(--surface) 96%,var(--bg));border:.8px solid color-mix(in srgb,var(--divider) 85%,transparent);box-shadow:0 7px 16px rgba(0,0,0,.12);z-index:5}
336.tabbar button{flex:1;min-width:0;min-height:58px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;border-radius:20px;color:var(--text3);font-size:12.5px;font-weight:500;padding:0 2px;white-space:nowrap;letter-spacing:-.1px}
337.tabbar button .i{font-size:22px}
338.tabbar button.on{color:var(--accent);font-weight:600;background:color-mix(in srgb,var(--accent-subtle) 90%,transparent)}
339
340/* sheets, toast */
341.scrim{position:absolute;inset:0;background:rgba(0,0,0,.35);z-index:20;display:flex;align-items:flex-end;animation:fade .18s ease-out}
342@keyframes fade{from{opacity:0}to{opacity:1}}
343.sheet{background:var(--bg);border-radius:20px 20px 0 0;width:100%;max-height:88%;display:flex;flex-direction:column;animation:up .22s ease-out}
344@keyframes up{from{transform:translateY(40px);opacity:.6}to{transform:none;opacity:1}}
345.sheet .grab{width:36px;height:5px;border-radius:3px;background:var(--divider);margin:8px auto 0}
346.sheet .sh{display:flex;align-items:center;justify-content:space-between;padding:12px 18px 8px}
347.sheet .sh .t{font-size:17px;font-weight:600}
348.sheet .sb{overflow-y:auto;padding:4px 18px 24px;display:flex;flex-direction:column;gap:14px}
349.sheet .ft{padding:12px 18px 18px;border-top:1px solid var(--divider);display:flex;flex-direction:column;gap:10px}
350.opt-row{display:flex;align-items:center;gap:12px;padding:13px 16px;width:100%;font-size:16px}
351.opt-row .i{font-size:22px;color:var(--text3)}
352.opt-row.on .i{color:var(--accent)}
353.opt-row.dn{color:var(--danger)}
354.opt-row:disabled{opacity:.4}
355.toast{position:absolute;
355left:50%;bottom:120px;transform:translateX(-50%);background:var(--text);color:var(--bg);padding:10px 16px;border-radius:999px;font-size:14px;font-weight:500;z-index:30;white-space:nowrap;box-shadow:0 6px 16px rgba(0,0,0,.2);animation:fade .18s}
356.center{text-align:center}
357.wand{position:absolute;right:0;top:0}
358.dnote{background:var(--surface);border:1px solid var(--divider);border-radius:12px;padding:12px 14px;font-size:14px;color:var(--text2);display:flex;gap:10px;align-items:flex-start}
359.dnote .i{color:var(--accent);margin-top:2px;flex:none}
360.safety{background:var(--danger-s);color:var(--danger);border-radius:10px;padding:12px 14px;display:flex;gap:10px;align-items:flex-start}
361.fill-note{font-size:13px;color:var(--text3);text-align:center}
362/* splash */
363.splash{position:absolute;inset:0;z-index:40;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;overflow:hidden;transition:opacity .4s ease;cursor:pointer}
364.splash.out{opacity:0;pointer-events:none}
365.splash::before,.splash::after{content:"";position:absolute;border-radius:50%;filter:blur(40px);pointer-events:none}
366.splash::before{width:320px;height:320px;left:-80px;top:-60px;background:color-mix(in srgb,var(--tint) 34%,transparent)}
367.splash::after{width:260px;height:260px;right:-70px;bottom:40px;background:color-mix(in srgb,var(--tint) 20%,transparent)}
368.splash .ring{position:relative;width:132px;height:132px;display:flex;align-items:center;justify-content:center}
369.splash .ring svg.o{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
370.splash .ring circle{fill:none;stroke:var(--accent);stroke-width:1.5;stroke-dasharray:408;stroke-dashoffset:408;animation:draw 1.1s .15s cubic-bezier(.4,0,.2,1) forwards}
371@keyframes draw{to{stroke-dashoffset:0}}
372.splash .ring img{width:96px;height:96px;animation:rise .7s ease-out both}
373@keyframes rise{from{transform:scale(.92)}to{transform:none}}
374.splash .n1{font-size:27px;font-weight:600;margin-top:22px;letter-spacing:.5px;animation:tighten .9s .3s ease-out both}
375.splash .n2{font-size:15px;color:var(--text2);margin-top:2px;animation:tighten .9s .45s ease-out both}
376.splash .n3{margin-top:28px;font-size:12px;font-weight:600;letter-spacing:1.4px;text-transform:uppercase;color:var(--accent);background:var(--accent-subtle);padding:7px 14px;border-radius:999px;animation:tighten .9s .8s ease-out both}
377@keyframes tighten{from{letter-spacing:3px;transform:translateY(6px)}to{transform:none}}
378@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
379/* Screen transitions: iOS-style push/pop and a tab cross-fade. Only #app animates; the
380   status bar and tab bar are separate layers so they stay still. */
381#app{view-transition-name:dn-screen}
382.sbar{view-transition-name:dn-sbar}
383.tabbar{view-transition-name:dn-tabbar}
384:root{view-transition-name:none}
385::view-transition-group(dn-screen){overflow:clip;border-radius:clamp(34px,10.8vw,51px)}
386::view-transition-old(dn-screen),::view-transition-new(dn-screen){height:100%;mix-blend-mode:normal}
387html[data-nav=forward]::view-transition-old(dn-screen){animation:dnOutLeft .34s cubic-bezier(.32,.72,0,1) both}
388html[data-nav=forward]::view-transition-new(dn-screen){animation:dnInRight .34s cubic-bezier(.32,.72,0,1) both;box-shadow:-10px 0 28px rgba(0,0,0,.10)}
389html[data-nav=back]::view-transition-old(dn-screen){animation:dnOutRight .3s cubic-bezier(.32,.72,0,1) both;z-index:2;box-shadow:-10px 0 28px rgba(0,0,0,.10)}
390html[data-nav=back]::view-transition-new(dn-screen){animation:dnInLeft .3s cubic-bezier(.32,.72,0,1) both}
391html[data-nav=fade]::view-transition-old(dn-screen){animation:dnFadeOut .16s ease both}
392html[data-nav=fade]::view-transition-new(dn-screen){animation:dnFadeIn .2s ease both}
393::view-transition-group(dn-sbar),::view-transition-group(dn-tabbar){animation-duration:.2s}
394@keyframes dnInRight{from{transform:translateX(100%)}to{transform:none}}
395@keyframes dnOutLeft{from{transform:none;opacity:1}to{transform:translateX(-28%);opacity:.6}}
396@keyframes dnInLeft{from{transform:translateX(-28%);opacity:.6}to{transform:none;opacity:1}}
397@keyframes dnOutRight{from{transform:none}to{transform:translateX(100%)}}
398@keyframes dnFadeOut{to{opacity:0}
398}
399@keyframes dnFadeIn{from{opacity:0}}
400@media (prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}
401
402</style>
403
404<div class="stage"><a class="demo-back" href="/psychiatry" aria-label="Return to the Psychiatry page">← Back to Psychiatry</a><div class="phone"><div class="screen" id="app"></div></div></div>
405<style>
406.demo-about{background:var(--bg);color:var(--text);padding:56px 20px 64px;border-top:1px solid var(--divider)}
407.demo-about__in{max-width:720px;margin:0 auto}
408.demo-about h1{font-size:clamp(1.6rem,1.2rem + 1.6vw,2.25rem);line-height:1.15;letter-spacing:-0.02em;margin:0 0 14px}
409.demo-about h2{font-size:1.125rem;margin:32px 0 10px}
410.demo-about p,.demo-about li{color:var(--text2);line-height:1.65}
411.demo-about ul{padding-left:1.2em;margin:0}
412.demo-about li{margin:4px 0}
413.demo-about a{color:var(--accent);font-weight:600}
414.demo-about__cta{display:flex;flex-wrap:wrap;gap:10px 20px;margin-top:28px}
415.demo-about__cta a{display:inline-flex;align-items:center;min-height:44px}
416</style>
417<section class="demo-about" aria-labelledby="demo-about-title"><div class="demo-about__in">
418<h1 id="demo-about-title">Dr. Notes for Psychiatrists: interactive demo</h1>
419<p>This is a working preview of the Dr. Notes for Psychiatrists app that runs in your browser. Tap through it the way you would on an iPhone: open a sample patient, start a new evaluation and move through each section of a psychiatric note. Everything shown is sample data, and nothing you enter is sent anywhere.</p>
420<h2>What you can try</h2>
421<ul>
422<li>Browse patients and today's appointments.</li>
423<li>Start a new evaluation and record the consultation, with a transcript.</li>
424<li>Work through Safety, Symptoms, History and the Mental Status Examination.</li>
425<li>Score PHQ-9 and GAD-7 inside the evaluation, alongside the patient's previous results.</li>
426<li>Write the formulation and plan, then review the finished note.</li>
427</ul>
428<h2>Using it in your practice</h2>
429<p>The full app keeps patient records on your device and works without an internet connection. Learn more about <a href="/psychiatry">Dr. Notes for Psychiatrists</a>, see how a <a href="/psychiatric-evaluation">psychiatric evaluation</a> is structured, or try the free <a href="/free-tools/phq-9-depression-screening">PHQ-9</a> and <a href="/free-tools/gad-7-anxiety-screening">GAD-7</a> screening tools.</p>
430<div class="demo-about__cta">
431<a href="https://apps.apple.com/in/app/dr-notes-for-psychiatrists/id6809348546">Download on the App Store</a>
432<a href="https://play.google.com/store/apps/details?id=com.drnotes.psychiatry&amp;hl=en_IN">Get it on Google Play</a>
433</div>
434</div></section>
435<script>
436/* ---------- icons ---------- */
437const ICONS={
438home:'<path d="M3 11l9-8 9 8v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/>',
439users:'<circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><circle cx="17" cy="9" r="2.5"/><path d="M15.5 14.5a5 5 0 0 1 6 5"/>',
440clip:'<rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 3h6v2H9zM9 12l2 2 4-4M9 17h6"/>',
441cliplist:'<rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 3h6v2H9zM9 11h6M9 15h6"/>',
442calclock:'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/><circle cx="16.5" cy="16.5" r="3.2"/><path d="M16.5 15v1.6l1.1.8"/>',
443gear:'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
444left:'<path d="M15 18l-6-6 6-6"/>',right:'<path d="M9 18l6-6-6-6"/>',down:'<path d="M6 9l6 6 6-6"/>',up:'<path d="M18 15l-6-6-6 6"/>',
445updown:'<path d="M8 9l4-4 4 4M8 15l4 4 4-4"/>',
446plus:'<path d="M12 5v14M5 12h14"/>',search:'<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/>',
447mic:'<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M8 21h8"/>',
448pause:'<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>',
449play:'<path d="M7 5l12 7-12 7z"/>',stop:'<rect x="6" y="6" width="12" height="12" rx="2"/>',
450cal:'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/>',
451calplus:'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4M12 13v5M9.5 15.5h5"/>',
452clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
453checkc:'<circle cx="12" cy="12" r="9"/><path d="M8.5 12.5l2.5 2.5 4.5-5"/>',
454check:'<path d="M5 12l5 5 9-10"/>',
455bubble:'<path d="M4 4h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H9l-5 4V5a1 1 0 0 1 1-1z"/><path d="M9 8.5c.3 2.3 1.9 3.9 4.2 4.2l1-1 1.5.6v1.2c-3.3.5-7-3.2-6.5-6.5h1.2z"/>',
456more:'<circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/>',
457heart:'<rect x="3" y="3" width="18" height="18" rx="3"/><path d="M12 16.5s-4.5-2.6-4.5-5.5a2.2 2.2 0 0 1 4.5-.8 2.2 2.2 0 0 1 4.5.8c0 2.9-4.5 5.5-4.5 5.5z"/>',
458doc:'<path d="M6 3h8l5 5v13H6z"/><path d="M14 3v5h5M9 13h6M9 17h6"/>',
459history:'<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5M12 8v4l3 2"/>',
460steth:'<path d="M5 3v6a5 5 0 0 0 10 0V3"/><path d="M10 14v2a5 5 0 0 0 10 0v-2"/><circle cx="20" cy="11" r="2"/>',
461grid:'<rect x="4" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1.5"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1.5"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1.5"/>',
462pencil:'<path d="M4 20l4-1L19 8l-3-3L5 16z"/>',
463shield:'<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/>',
464shieldck:'<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/>',
465wave:'<path d="M3 12h2l2-6 3 12 3-9 2 6 2-3h4"/>',
466waverect:'<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M6 12h2l1.5-3 2 6 2-5 1.5 2H18"/>',
467x:'<path d="M6 6l12 12M18 6L6 18"/>',
468info:'<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>',
469warn:'<path d="M12 3l10 18H2z"/><path d="M12 10v4M12 17h.01"/>',
470spark:'<path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z"/><path d="M19 16l.7 1.8 1.8.7-1.8.7L19 21l-.7-1.8-1.8-.7 1.8-.7z"/>',
471copy:'<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/>',
472upright:'<path d="M7 17L17 7M8 7h9v9"/>',
473turn:'<path d="M5 4v8a2 2 0 0 0 2 2h12"/><path d="M15 10l4 4-4 4"/>',
474userplus:'<circle cx="10" cy="8" r="4"/><path d="M3 21a7 7 0 0 1 14 0M19 8v6M16 11h6"/>',
475trash:'<path d="M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14"/>',
476sqpen:'<path d="M12 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-6"/><path d="M18 3l3 3-9 9H9v-3z"/>',
477phone:'<path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2z"/>',
478idcard:'<rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="9" cy="11" r="2"/><path d="M6 16a3 3 0 0 1 6 0M14 10h4M14 14h4"/>',
479export:'<path d="M12 3v12M8 7l4-4 4 4"/><path d="M4 14v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"/>',
480back15:'<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/>',
481fwd15:'<path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/>',
482lock:'<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>',
483};
484const I=(n,cls='')=>`<svg class="i ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n]}</svg>`;
485const IF=n=>I(n,'f');
486const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
487
488/* ---------- clock (fixed demo time so the schedule reads the same every visit) ---------- */
489const NOW=new Date();NOW.setHours(13,5,0,0);
490const dAdd=(d,days,h,m)=>{const x=new Date(d);x.setDate(x.getDate()+days);if(h!=null)x.setHours(h,m||0,0,0);return x;};
491const today=(h,m)=>dAdd(NOW,0,h,m);
492const MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
493const DAYS=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
494const fmtTime=d=>{let h=d.getHours(),m=d.getMinutes();const ap=h>=12?'PM':'AM';h=h%12||12;return `${h}
494:${String(m).padStart(2,'0')} ${ap}`;};
495const fmtDate=d=>`${d.getDate()} ${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
496const fmtDateShort=d=>`${d.getDate()} ${MONTHS[d.getMonth()]}`;
497const fmtDateDow=d=>`${DAYS[d.getDay()].slice(0,3)}, ${fmtDate(d)}`;
498const fmtDateUS=d=>`${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
499const sameDay=(a,b)=>a.toDateString()===b.toDateString();
500const dayDiff=(a,b)=>Math.round((dAdd(b,0,0,0)-dAdd(a,0,0,0))/864e5);
501function relDays(d){const n=dayDiff(d,NOW);if(n<=0)return 'Today';if(n===1)return 'Yesterday';if(n<14)return n+'d ago';if(n<60)return Math.round(n/7)+'w ago';if(n<365)return Math.round(n/30)+'mo ago';return Math.round(n/365)+'y ago';}
502function dayLabel(d){const n=dayDiff(NOW,d);if(n===0)return 'Today';if(n===1)return 'Tomorrow';if(n===-1)return 'Yesterday';return `${DAYS[d.getDay()].slice(0,3)} ${fmtDateShort(d)}`;}
503function dayLabelLong(d){const n=dayDiff(NOW,d);if(n===0)return 'Today';if(n===1)return 'Tomorrow';if(n===-1)return 'Yesterday';return `${DAYS[d.getDay()]} ${d.getDate()} ${MONTHS[d.getMonth()]}`;}
504const durText=m=>m<60?m+'m':(m%60===0?(m/60)+'h':(m/60).toFixed(1)+'h');
505
506/* ---------- demo data ---------- */
507const CLINICIAN={name:'Dr. James Whitfield',short:'Dr. James',clinic:'MindCare Clinic',address:'12 Harbour Road, Chennai 600001',qual:'MBBS, MD Psychiatry'};
508const AV_HUES=['#4A6E8A','#4F7A6A','#8A6A4A','#5E6A70','#8A5A66','#54708F','#707A55','#4C7C86'];
509const SCHED_HUES=['var(--h0)','var(--h1)','var(--h2)','var(--h3)','var(--h4)'];
510let uid=100;const nid=()=>'id'+(uid++);
511function mkPatient(o){return Object.assign({id:nid(),email:'',emergency:null,blood:'',clinic:''},o,{dob:o.dob||dAdd(NOW,-365*o.age-40)});}
512const DATA={
513 patients:[
514  mkPatient({name:'Alex Morgan',age:34,gender:'Male',phone:'9000000000',email:'[email protected]',lastVisit:today(9,58),created:dAdd(NOW,-120),mrn:'884406',hue:0}),
515  mkPatient({name:'Priya Raman',age:28,gender:'Female',phone:'9000000001',email:'[email protected]',lastVisit:dAdd(NOW,-4),created:dAdd(NOW,-60),mrn:'517220',hue:6}),
516  mkPatient({name:'Daniel Okafor',age:47,gender:'Male',phone:'9000000002',email:'[email protected]',lastVisit:dAdd(NOW,-11),created:dAdd(NOW,-240),mrn:'302918',hue:2}),
517  mkPatient({name:'Meera Iyer',age:41,gender:'Female',phone:'9000000003',email:'[email protected]',lastVisit:dAdd(NOW,-18),created:dAdd(NOW,-320),mrn:'640177',hue:4}),
518  mkPatient({name:'Jon Bell',age:52,gender:'Male',phone:'9000000004',email:'[email protected]',lastVisit:dAdd(NOW,-7),created:dAdd(NOW,-190),mrn:'218905',hue:1}),
519  mkPatient({name:'Sofia Alvarez',age:23,gender:'Female',phone:'9000000005',email:'[email protected]',lastVisit:dAdd(NOW,-31),created:dAdd(NOW,-75),mrn:'773341',hue:5}),
520  mkPatient({name:'Noah Williams',age:38,gender:'Non-binary',phone:'9000000006',email:'[email protected]',lastVisit:dAdd(NOW,-2),created:dAdd(NOW,-140),mrn:'455082',hue:3}),
521 ],
522 appointments:[],evaluations:[],recentSearch:[]
523};
524const P=name=>DATA.patients.find(p=>p.name===name);
525const VISIT={initial:{full:'Initial Psychiatric Evaluation',short:'Initial evaluation',dur:60},followUp:{full:'Follow-up',short:'Follow-up',dur:30},medReview:{full:'Medication Review',short:'Medication review',dur:20},therapy:{full:'Therapy Session',short:'Therapy session',dur:50},crisis:{full:'Crisis Assessment',short:'Crisis assessment',dur:45}};
526const EVAL_TYPES=[['followUp','Follow-up Evaluation','Review progress, symptoms and treatment plan','Follow-up Visit'],['medReview','Medication Review','Review medication response, adherence and side effects','Follow-up Visit'],['therapy','Therapy Session','Document psychotherapy session and progress','Follow-up Visit'],['risk','Risk Review','Review safety and current risk factors','Follow-up Visit'],['initial','Initial Evaluation','Comprehensive psychiatric assessment','Initial Psychiatric Assessment']];
527function addAppt(name,at,type,notes='',status='Scheduled',dur){DATA.appointments.push({id:nid(),pid:P(name).id,name,at,type,dur:dur||VISIT[type].dur,notes,status,urgent:false,booked:dAdd(at,-9,8,2)});}
528addAppt('Alex Morgan',today(9,30),'followUp','Review response to behavioural activation.','Completed');
529addAppt('Meera Iyer',today(8,30),'initial','Review panic symptoms and treatment goals.','Scheduled',45);
530addAppt('Jon Bell',today(10,30),'followUp','Review sleep diary and medication response.');
531addAppt('Priya Raman',today(11,0),'medReview','Discuss starting an SSRI.');
532addAppt('Sofia Alvarez',today(12,15),'therapy','CBT formulation and exposure planning.');
533addAppt('Noah Williams',today(13,30),'medReview','Review relapse-prevention plan.');
534addAppt('Daniel Okafor',today(14,15),'initial','Referred by GP for low mood and alcohol use.');
535addAppt('Alex Morgan',today(15,45),'therapy','Behavioural activation, session three.');
536addAppt('Meera Iyer',today(16,0),'followUp','Brief check-in after initial assessment.');
537addAppt('Priya Raman',today(17,0),'followUp');
538addAppt('Jon Bell',today(18,0),'therapy','Behavioural activation review.');
539addAppt('Priya Raman',dAdd(NOW,3,10,0),'therapy');
540addAppt('Sofia Alvarez',dAdd(NOW,7,9,0),'initial','New consultation next week.');
541addAppt('Priya Raman',dAdd(NOW,-1,10,0),'followUp','','Completed');
542addAppt('Noah Williams',dAdd(NOW,-2,13,30),'medReview','Relapse-prevention planning.','Completed');
543addAppt('Jon Bell',dAdd(NOW,-7,10,30),'followUp','','Completed');
544addAppt('Daniel Okafor',dAdd(NOW,-11,14,0),'initial','','Cancelled');
545
546const SAMPLE_TRANSCRIPT=`Clinician: How have things been since your last visit?
547
548Patient: Honestly, the anxiety has been worse, especially in the evenings. I keep thinking about work and I am sleeping only around five hours.
549
550Clinician: When you say worse, is it the worry itself or the physical side of it?
551
552Patient: Both. My chest gets tight and I lie there going over everything I did not finish. Then I am exhausted the next day and get less done, which makes it worse.
553
554Clinician: Have there been any thoughts that life is not worth living, or of harming yourself?
555
556Patient: Sometimes I think it would be easier not to wake up. I would never do anything though. My wife and I have been talking about it and she has been really supportive.
557
558Clinician: Thank you for telling me. We will review the safety plan, focus on sleep and activity, and meet again in two weeks.`;
559
560const SAMPLE_DRAFT={
561 presentingConcern:'Patient reports worsening anxiety and low mood over the past three months, associated with occupational stress and poor sleep.',
562 intervalNote:'Symptoms gradually worsened over the last three months, with reduced activity and increased evening rumination.',
563 todayMood:'Low',todaySleep:'Poor — approximately five hours nightly',todayAppetite:'Reduced',todayEnergy:'Low',todayConcentration:'Reduced',todayFunction:'Mild functional impairment at work',
564 suicidalThoughts:'Passive',passiveDeathWish:'Present',currentIntent:'Denied',specificPlan:'Denied',accessMeans:'Denied',previousAttempts:'Denied',
565 recentSelfHarm:'Denied',harmToOthers:'Denied',severeAgitation:'Denied',psychosisManiaRisk:'Denied',unableCare:'Denied',substanceIntoxicationConcern:'Denied',
566 clinicianRisk:'Moderate',protectiveFactors:'Supportive spouse; good treatment engagement; future-oriented; stable housing.',
567 safetyNote:'Increased distress following work-related stress. Clinician reviewed warning signs and crisis resources.',
568 selectedDomains:['Depression','Anxiety','Sleep'],
569 domainNotes:{Depression:'Low mood, reduced interest, fatigue and reduced motivation for three months.',Anxiety:'Excessive worry about work, worse in the evenings, with physical tension.',Sleep:'Difficulty initiating sleep and early waking; averages approximately five hours.'},
570 psychiatricHistory:'No previous psychiatric admissions. Intermittent anxiety since university; no prior suicide attempts.',
571 medicationHistory:'Previous brief trial of counselling only; no prior psychotropic medication.',
572 medicalHistory:'No significant medical conditions reported. No current neurological symptoms.',
573 admissionsStatus:'None reported',substanceUseStatus:'Past use',substanceUse:'Alcohol socially, one to two drinks on weekends. Denies tobacco and recreational drug use.',
574 familyHistoryStatus:'Present',familyHistory:'Mother treated for anxiety; no known family history of psychosis or bipolar disorder.',
575 socialHistory:'Lives with supportive spouse in stable housing. Good social network, recently reducing pleasurable activity.',
576 developmentalStatus:'No concerns reported',personalityTraits:['Sociable','Perfectionistic'],
577 premorbidFunctioning:'Previously described as sociable, conscientious, organised and generally optimistic.',
578 personalityNotes:'High personal standards and a tendency to ruminate when demands increase.',
579 traumaAdversity:'No significant trauma or adversity disclosed in this assessment.',
580 currentMedication:'None currently prescribed.',adherence:'Not currently prescribed',response:'Too early to assess',sideEffects:'Not applicable; no current psychotropic medication.',
581 psychologicalTreatment:'No current therapy; interested in behavioural activation.',
582 appearance:'Well groomed',behavior:'Cooperative',eyeContact:'Appropriate',psychomotor:'Normal',speech:'Normal rate and volume',mood:'Anxious',affect:'Restricted',
583 thoughtProcess:'Linear and goal-directed',thoughtContent:'Suicidal thoughts',thoughtContentDetails:'No delusions or obsessional content elicited; passive thoughts of death without intent or plan.',
584 perception:'No perceptual disturbance',cognition:'Attention reduced',orientation:'Alert and oriented',insight:'Good',judgment:'Intact',impulseControl:'Intact',
585 mseNotes:'Engaged throughout interview. No evidence of responding to internal stimuli.',
586 assessmentResults:[
587  {instrument:'phq9',title:'PHQ-9',score:16,max:27,responses:{1:2,2:2,3:2,4:2,5:2,6:2,7:1,8:2,9:1}},
588  {instrument:'gad7',title:'GAD-7',score:13,max:21,responses:{1:2,2:2,3:2,4:2,5:2,6:2,7:1}},
589  {instrument:'pcptsd5',title:'PC-PTSD-5',score:2,max:5,responses:{1:1,2:1,3:0,4:0,5:0},trauma:true}
590 ],
591 formulation:'Presentation is consistent with a depressive episode with clinically significant anxiety symptoms in the context of occupational stress.',
592 predisposing:'Family history of anxiety and perfectionistic personality traits.',precipitating:'Recent increase in occupational responsibilities and interpersonal conflict at work.',
593 perpetuating:'Poor sleep, avoidance, rumination and reduced pleasurable activity.',protective:'Supportive spouse, stable employment, good insight and willingness to engage in treatment.',
594 workingDiagnosis:'Major depressive episode with anxiety symptoms.',differentials:'Adjustment disorder with mixed anxiety and depressed mood.',
595 planMedication:'Discussed medication options, expected benefits, side effects and the option of review before commencing treatment.',
596 planPsychological:'Offer psychological therapy focused on behavioural activation, anxiety management and rumination.',
597 planInvestigations:'Consider routine medical review and baseline investigations if symptoms persist or medication is commenced.',
598 planReferral:'No urgent referral required; provide therapy referral options.',
599 psychoeducationStatus:'Discussed',psychoeducationDetails:'Discussed depression, anxiety, recovery expectations and the relationship between sleep, activity and mood.',
600 safetyPlanStatus:'Reviewed',planSafety:'Patient advised to contact spouse and treating team if distress escalates and to seek urgent help if thoughts become active or intent develops.',
601 planFollowUp:'2 weeks',planDisposition:'Continue outpatient follow-up',
602 planOther:'Sleep hygiene: consistent wake time, reduce evening screen use and avoid caffeine after lunch. Behavioural activation: schedule two manageable pleasurable or mastery activities before review.',
603 additionalClinicalNote:'Alex engaged well with the interview and demonstrated good insight. The patient understands the proposed plan and agrees to monitor sleep, activity and suicidal thinking.'
604};
605const clone=o=>JSON.parse(JSON.stringify(o));
606function draftVariant(base,over){const d=clone(base);Object.assign(d,over);return d;}
607const PREV_DRAFT=draftVariant(SAMPLE_DRAFT,{todaySleep:'Poor — approximately four hours nightly',todayFunction:'Moderate functional impairment at work',planFollowUp:'4 weeks',intervalNote:'',
608 assessmentResults:[{instrument:'phq9',title:'PHQ-9',score:19,max:27,responses:{1:2,2:3,3:2,4:2,5:2,6:2,7:2,8:2,9:2}},{instrument:'gad7',title:'GAD-7',score:16,max:21,responses:{1:3,2:2,3:2,4:2,5:2,6:2,7:3}}]});
609const PRIYA_DRAFT={presentingConcern:'Initial assessment for persistent panic symptoms and avoidance of public transport.',todayMood:'Anxious',todaySleep:'Variable — initial insomnia on nights before work',todayFunction:'Avoiding public transport; commuting by taxi',
610 suicidalThoughts:'Denied',recentSelfHarm:'Denied',harmToOthers:'Denied',clinicianRisk:'Low',selectedDomains:['Anxiety','Sleep'],domainNotes:{Anxiety:'Recurrent unexpected panic attacks with anticipatory anxiety; avoidance of crowded trains.',Sleep:'Difficulty falling asleep before work days.'},
611 appearance:'Well groomed',behavior:'Cooperative',speech:'Normal rate and volume',mood:'Anxious',affect:'Appropriate',thoughtProcess:'Logical',thoughtContent:'No abnormal content reported',perception:'No perceptual disturbance',insight:'Good',judgment:'Intact',
612 assessmentResults:[{instrument:'gad7',title:'GAD-7',score:12,max:21,responses:{1:2,2:2,3:2,4:2,5:1,6:1,7:2}}],
613 formulation:'Panic disorder with developing agoraphobic avoidance, maintained by safety behaviours and catastrophic misinterpretation of bodily sensations.',
614 workingDiagnosis:'Panic disorder with agoraphobic avoidance.',differentials:'Generalized anxiety disorder; cardiac causes excluded by GP.',planPsychological:'Refer for cognitive behavioural therapy with interoceptive exposure.',psychoeducationStatus:'Discussed',psychoeducationDetail
614s:'Panic cycle and the role of avoidance.',safetyPlanStatus:'Not required',planFollowUp:'4 weeks',planDisposition:'Continue outpatient follow-up'};
615function addEval(name,daysAgo,visitType,draft,extra={}){const p=P(name);DATA.evaluations.push(Object.assign({id:nid(),pid:p.id,date:dAdd(NOW,-daysAgo,daysAgo?10:9,daysAgo?15:58),visitType,draft,transcript:null,seconds:0},extra));}
616addEval('Alex Morgan',28,'Initial Psychiatric Assessment',PREV_DRAFT,{seconds:1320});
617addEval('Alex Morgan',0,'Follow-up Visit',clone(SAMPLE_DRAFT),{transcript:SAMPLE_TRANSCRIPT,seconds:483});
618addEval('Priya Raman',4,'Initial Psychiatric Assessment',PRIYA_DRAFT,{seconds:1560});
619addEval('Meera Iyer',18,'Initial Psychiatric Assessment',draftVariant(PRIYA_DRAFT,{workingDiagnosis:'Panic disorder; assess agoraphobic avoidance.',selectedDomains:['Anxiety','Sleep','Functioning']}));
620addEval('Jon Bell',35,'Initial Psychiatric Assessment',clone(PREV_DRAFT));
621addEval('Jon Bell',7,'Follow-up Visit',draftVariant(SAMPLE_DRAFT,{presentingConcern:'Follow-up for anxiety and low mood; sleep and daily routine have improved since the previous visit.',todayMood:'Improving',todaySleep:'Six to seven hours most nights',clinicianRisk:'Low',planFollowUp:'4 weeks'}));
622addEval('Sofia Alvarez',31,'Initial Psychiatric Assessment',clone(PRIYA_DRAFT));
623addEval('Noah Williams',2,'Follow-up Visit',draftVariant(SAMPLE_DRAFT,{presentingConcern:'Medication review: mood remains stable; requests support with relapse-prevention planning.',todayMood:'Stable',suicidalThoughts:'Denied',passiveDeathWish:'Denied',clinicianRisk:'Low',planFollowUp:'3 months'}));
624
625/* ---------- scales ---------- */
626const SCALES={
627 phq9:{id:'phq9',title:'PHQ-9',full:'Patient Health Questionnaire 9-item',desc:'Depression symptom screening',icon:'cliplist',tint:'var(--accent)',domain:'Depression',time:'Over the last 2 weeks',max:27,
628  opts:[['Not at all',0],['Several days',1],['More than half the days',2],['Nearly every day',3]],
629  q:['Little interest or pleasure in doing things','Feeling down, depressed, or hopeless','Trouble falling or staying asleep, or sleeping too much','Feeling tired or having little energy','Poor appetite or overeating','Feeling bad about yourself — or that you are a failure or have let yourself or your family down','Trouble concentrating on things, such as reading the newspaper or watching television','Moving or speaking so slowly that other people could have noticed — or the opposite, being so fidgety or restless that you have been moving around a lot more than usual','Thoughts that you would be better off dead, or of hurting yourself in some way'],
630  bands:[[4,'Minimal or no depressive symptoms'],[9,'Mild depressive symptoms'],[14,'Moderate depressive symptoms'],[19,'Moderately severe depressive symptoms'],[27,'Severe depressive symptoms']]},
631 gad7:{id:'gad7',title:'GAD-7',full:'Generalized Anxiety Disorder 7-item',desc:'Anxiety symptom screening',icon:'wave',tint:'var(--success)',domain:'Anxiety',time:'Over the last 2 weeks',max:21,
632  opts:[['Not at all',0],['Several days',1],['More than half the days',2],['Nearly every day',3]],
633  q:['Feeling nervous, anxious, or on edge','Not being able to stop or control worrying','Worrying too much about different things','Trouble relaxing','Being so restless that it is hard to sit still','Becoming easily annoyed or irritable','Feeling afraid, as if something awful might happen'],
634  bands:[[4,'Minimal anxiety symptoms'],[9,'Mild anxiety symptoms'],[14,'Moderate anxiety symptoms'],[21,'Severe anxiety symptoms']]},
635 pcptsd5:{id:'pcptsd5',title:'PC-PTSD-5',full:'Primary Care PTSD Screen for DSM-5',desc:'Trauma-related symptom screening',icon:'shield',tint:'var(--h0)',domain:'PTSD / Trauma',time:'Over the past month',max:5,gate:true,
636  gateQ:'Sometimes things happen to people that are unusually or especially frightening, horrible, or traumatic. In your life, have you ever had any experience that was that frightening, horrible, or traumatic?',
637  opts:[['No',0],['Yes',1]],
638  q:['Had nightmares about the event(s) or thought about the event(s) when you did not want to?','Tried hard not to think about the event(s) or went out of your way to avoid situations that reminded you of the event(s)?','Been constantly on guard, watchful, or easily startled?','Felt numb or detached from people, activities, or your surroundings?','Felt guilty or unable to stop blaming yourself or others for the event(s) or any problems the event(s) may have caused?'],
639  bands:[[2,'Screen negative'],[5,'Positive PTSD screen']]},
640 pcl5:{id:'pcl5',title:'PCL-5',full:'PTSD Checklist for DSM-5',desc:'PTSD symptom severity assessment',icon:'waverect',tint:'var(--h0)',domain:'PTSD severity',time:'In the past month, how much were you bothered by:',max:80,
641  opts:[['Not at all',0],['A little bit',1],['Moderately',2],['Quite a bit',3],['Extremely',4]],
642  q:['Repeated, disturbing, and unwanted memories of the stressful experience?','Repeated, disturbing dreams of the stressful experience?','Suddenly feeling or acting as if the stressful experience were actually happening again?','Feeling very upset when something reminded you of the stressful experience?','Having strong physical reactions when something reminded you of the stressful experience?','Avoiding memories, thoughts, or feelings related to the stressful experience?','Avoiding external reminders of the stressful experience?','Trouble remembering important parts of the stressful experience?','Having strong negative beliefs about yourself, other people, or the world?','Blam
642ing yourself or someone else for the stressful experience or what happened after it?','Having strong negative feelings such as fear, horror, anger, guilt, or shame?','Loss of interest in activities that you used to enjoy?','Feeling distant or cut off from other people?','Trouble experiencing positive feelings?','Irritable behavior, angry outbursts, or acting aggressively?','Taking too many risks or doing things that could cause you harm?','Being superalert or watchful or on guard?','Feeling jumpy or easily startled?','Having difficulty concentrating?','Trouble falling or staying asleep?'],
643  bands:[[80,'PTSD symptom severity score']]}
644};
645const SCALE_ORDER=['phq9','gad7','pcptsd5','pcl5'];
646const severity=(sid,score)=>{const s=SCALES[sid];for(const [max,l] of s.bands)if(score<=max)return l;return s.bands.at(-1)[1];};
647
648/* ---------- option lists ---------- */
649const OPT={
650 appearance:['Well groomed','Appropriately dressed','Dishevelled','Poor hygiene','Unkempt','Inappropriately dressed'],
651 behavior:['Cooperative','Calm','Guarded','Withdrawn','Agitated','Restless','Hostile','Disinhibited','Poor eye contact'],
652 speech:['Normal rate and volume','Rapid','Slow','Pressured','Reduced volume','Loud','Poverty of speech','Mute'],
653 mood:['Euthymic','Anxious','Depressed','Irritable','Elevated','Low','Angry'],
654 affect:['Congruent','Appropriate','Restricted','Blunted','Flat','Labile','Incongruent','Anxious'],
655 thoughtProcess:['Linear and goal-directed','Logical','Circumstantial','Tangential','Flight of ideas','Loose associations','Disorganised','Thought blocking'],
656 thoughtContent:['No abnormal content reported','Delusional ideas','Obsessions','Preoccupations','Overvalued ideas','Suicidal thoughts','Homicidal thoughts'],
657 perception:['No perceptual disturbance','Auditory hallucinations','Visual hallucinations','Other hallucinations','Illusions'],
658 orientation:['Alert and oriented','Partially oriented','Disoriented','Fluctuating','Drowsy'],
659 insight:['Good','Fair','Partial','Poor','Absent'],judgment:['Intact','Fair','Impaired','Poor'],
660 eyeContact:['Appropriate','Reduced','Avoidant','Intense'],psychomotor:['Normal','Agitation','Retardation','Restless','Tremor'],
661 cognition:['Grossly intact','Attention reduced','Memory impairment','Concentration reduced','Requires formal testing'],impulseControl:['Intact','Fair','Impaired','Poor'],
662 suicidalThoughts:['Denied','Passive','Active','Present','Unknown'],recentSelfHarm:['Denied','Present','Recent','Historical','Unknown'],
663 dpu:['Denied','Present','Unknown'],clinicianRisk:['Low','Moderate','High','Immediate concern'],
664 todayMood:['Euthymic','Low','Anxious','Irritable','Elevated','Labile'],todaySleep:['Normal','Reduced','Increased','Initial insomnia','Middle insomnia','Early morning waking','Markedly disturbed'],
665 todayAppetite:['Normal','Reduced','Increased','Markedly reduced'],todayEnergy:['Normal','Reduced','Low','Increased'],todayConcentration:['Normal','Mildly reduced','Reduced','Markedly reduced'],todayFunction:['Unaffected','Mildly affected','Moderately affected','Markedly affected'],
666 admissions:['None reported','Yes','Unknown'],substance:['None reported','Current use','Past use','Unknown'],family:['None reported','Present','Unknown'],
667 adherence:['Good','Partial','Poor','Not currently prescribed'],response:['Good response','Partial response','No response','Worsening','Too early to assess'],
668 psychoeducation:['Not discussed','Discussed','Not applicable'],safetyPlan:['Not required','Reviewed','Updated','Created'],
669 disposition:['Continue outpatient follow-up','Discharged','Refer to specialist service','Admit','Crisis team involvement'],
670 followUp:['1 week','2 weeks','4 weeks','6 weeks','8 weeks','3 months','As needed'],
671 domains:['Depression','Anxiety','Mania / Bipolar','Psychosis','OCD','Trauma / PTSD','ADHD','Eating concerns','Substance use','Cognitive concerns','Sleep','Behavioural concerns','Other'],
672 traits:['Sociable','Reserved','Anxious','Perfectionistic','Impulsive','Emotionally reactive','Dependent','Rigid','Suspicious','Other'],
673 developmental:['No concerns reported','Developmental concerns present','Not assessed']
674};
675const riskTone=r=>(r==='High'||r==='Immediate concern')?'dn':r==='Moderate'?'wn':r==='Low'?'ok':'nt';
676
677/* ---------- derived helpers ---------- */
678const initials=n=>
678n.trim().split(/\s+/).filter(Boolean).map(w=>w[0].toUpperCase()).filter((_,i,a)=>i===0||i===a.length-1).join('');
679const avatar=(p,size,hue)=>`<span class="av" style="--hue:${hue||AV_HUES[p.hue%8]};width:${size}px;height:${size}px;font-size:${Math.round(size*0.36)}px">${esc(initials(p.name))}</span>`;
680const pat=id=>DATA.patients.find(p=>p.id===id);
681const evalsFor=pid=>DATA.evaluations.filter(e=>e.pid===pid).sort((a,b)=>b.date-a.date);
682const apptEnd=a=>new Date(a.at.getTime()+a.dur*6e4);
683function apptDisplay(a){if(a.status==='Completed')return 'Completed';if(a.status==='Cancelled')return 'Cancelled';return apptEnd(a)<NOW?'Overdue':'Scheduled';}
684function apptPill(a){const s=apptDisplay(a);return s==='Scheduled'?['Upcoming','']:s==='Overdue'?['Overdue','wn']:s==='Completed'?['Done','ok']:['Cancelled','nt'];}
685const todayAppts=()=>DATA.appointments.filter(a=>sameDay(a.at,NOW)).sort((a,b)=>a.at-b.at);
686function scaleSummary(d){return (d.assessmentResults||[]).map(r=>`${r.title} ${r.score}`).join(' · ');}
687function scoreOf(d,id){const r=(d.assessmentResults||[]).find(r=>r.instrument===id);return r?`${r.score}/${r.max}`:'';}
688/* ---------- state + router ---------- */
689const TABS=[['home','Home','home'],['patients','Patients','users'],['neweval','New Eval','clip'],['appts','Appts','calclock'],['settings','Settings','gear']];
690const state={tab:'home',stacks:{home:[{s:'home'}],patients:[{s:'patients'}],neweval:[{s:'chooser'}],appts:[{s:'appts'}],settings:[{s:'settings'}]},sheet:null,session:null,pendingScale:null,toast:null,scaleRun:null,query:'',chooserQ:''};
691const stack=()=>state.stacks[state.tab];
692const topS=()=>stack().at(-1);
693const A={};
694A.tab=t=>{if(state.tab===t){state.stacks[t]=state.stacks[t].slice(0,1);state.chooserQ='';}else state.tab=t;render(true);};
695A.push=(s,p={})=>{stack().push(Object.assign({s},p));render(true);};
696A.pop=()=>{if(stack().length>1)stack().pop();render(true);};
697A.popTo=s=>{const st=stack();const i=st.findIndex(x=>x.s===s);if(i>=0)st.length=i+1;render(true);};
698A.sheet=(type,p={})=>{state.sheet=Object.assign({type},p);render();};
699A.closeSheet=()=>{state.sheet=null;render();};
700A.toast=(m,ms=1800)=>{state.toast=m;render();clearTimeout(A._tt);A._tt=setTimeout(()=>{state.toast=null;render();},ms);};
701const FULLSCREEN=new Set(['entry','workspace','review','transcript','scale']);
702
703/* ---------- chrome ---------- */
704function header(o){const back=o.back!==false?`<button class="disc" onclick="A.pop()" aria-label="Back">${I('left')}</button>`:(o.mark?`<span class="mark"><img src="/assets/images/drnotes-demo-splash-logo.png" alt="" decoding="async"></span>`:'');
705 const tr=(o.trailing||[]).map(b=>`<button class="disc" onclick="${b.on}" aria-label="${esc(b.label)}">${I(b.icon)}</button>`).join('');
706 return `<div class="hdr ${o.tint?'tint':''}" style="${o.tint?'--tint:'+o.tint:''}"><div class="hrow">${back}<span class="sp"></span>${tr}</div>${o.title?`<div class="htitle">${o.title}</div>`:''}${o.sub?`<div class="hsub">${o.sub}</div>`:''}</div>`;}
707function tabbar(){return `<nav class="tabbar">${TABS.map(([id,l,ic])=>`<button class="${state.tab===id?'on':''}" onclick="A.tab('${id}')">${I(ic)}<span>${l}</span></button>`).join('')}</nav>`;}
708function evalHeader(title,backAction='A.pop()',trailing=''){return `<div class="hdr eval-header"><div class="hrow"><button class="disc" onclick="${backAction}" aria-label="Back">${I('left')}</button><div class="eval-header-title">${title}</div><span class="sp"></span>${trailing}</div></div>`;}
709function secHead(title,sub,icon,trailing=''){return `<div class="sec">${icon?`<span class="ico">${I(icon)}</span>`:''}<div><div class="st">${title}</div>${sub?`<div class="sc">${sub}</div>`:''}</div><span class="sp"></span>${trailing}</div>`;}
710function tlRow(a,i,onclick){const p=pat(a.pid);const hue=SCHED_HUES[i%5];const [pl,cls]=apptPill(a);const [t,ap]=fmtTime(a.at).split(' ');
711 return `<button class="tl" style="--hue:${hue}" onclick="${onclick}"><span class="tm"><b>${t}</b><span>${ap}</span></span><span class="mk"></span>${avatar(p,36,hue)}<span class="main"><div class="t1 ellip">${esc(a.name)}</div><div class="callout">${VISIT[a.type].short}</div></span><span class="pl ${cls}">${pl}</span></button>`;}
712function timeOfDayHue(){const h=NOW.getHours();return h>=5&&h<11?'var(--h4)':h<16?'var(--h2)':h<20?'var(--h1)':'var(--h0)';}
713
714/* ---------- Home ---------- */
715function screenHome(){const h=NOW.getHours();const greet=h<12?'Good morning':h<16?'Good afternoon':'Good evening';
716 const live=todayAppts().filter(a=>a.status!=='Cancelled');const inProg=live.find(a=>a.at<=NOW&&apptEnd(a)>NOW);const next=live.find(a=>a.at>NOW);const seen=live.filter(a=>a.status==='Completed').length;
717 let sub;if(inProg){const rest=live.filter(a=>a.at>NOW).length;sub=`Now · ${inProg.name.split(' ')[0]}${rest?` · ${rest} to come`:''}`;}else if(next){const rest=live.filter(a=>a.at>next.at).length;sub=`Next · ${next.name.split(' ')[0]} at ${fmtTime(next.at)}${rest?` · ${rest} more`:''}`;}else if(live.length)sub=`Day complete · ${seen} seen`;else sub=`${DAYS[NOW.getDay()]} ${NOW.getDate()} ${MONTHS[NOW.getMonth()]}`;
718 const slots=todayAppts().filter(a=>a.status==='Scheduled'&&apptEnd(a)>NOW).slice(0,3);
719 const all=todayAppts();const mins=all.reduce((s,a)=>s+a.dur,0);
720 return header({back:false,mark:true,title:`${greet}, ${esc(CLINICIAN.short)}`,sub,tint:timeOfDayHue()})+`<div class="content">
721 ${secHead("Today's Schedule",'','cal',`<button class="pill" onclick="A.tab('appts')">View all ${I('upright')}</button>`)}
722 ${slots.length?`<div class="tlist">${slots.map((a,i)=>tlRow(a,i,`A.openAppt('${a.id}')`)).join('')}</div>`:`<div class="card"><div class="empty"><div class="t">Nothing further today</div><div class="callout">
722Today's appointments have all finished.</div><button class="btn s" style="width:auto;padding:0 16px" onclick="A.tab('appts')">Open appointments</button></div></div>`}
723 <div class="card" style="padding:0"><div class="counters">
724  <div class="ct" style="--tint:var(--accent)"><span class="ico">${I('users')}</span><div><b>${all.length}</b><span>Appts</span></div></div>
725  <div class="ct" style="--tint:var(--success)"><span class="ico">${I('checkc')}</span><div><b>${all.filter(a=>a.type==='followUp').length}</b><span>Follow-ups</span></div></div>
726  <div class="ct" style="--tint:var(--info)"><span class="ico">${I('clock')}</span><div><b>${mins?durText(mins):'—'}</b><span>Session</span></div></div>
727 </div></div>
728 <div class="acts">
729  <button class="act" onclick="A.tab('neweval')"><span class="tile fill" style="width:34px;height:34px;font-size:17px">${I('sqpen')}</span><span class="t">New Evaluation ${I('right')}</span><span class="cap">Record a consultation</span></button>
730  <button class="act" onclick="A.push('tools')"><span class="tile" style="--tint:var(--h4);width:34px;height:34px;font-size:17px">${I('grid')}</span><span class="t">Quick Tools ${I('right')}</span><span class="cap">Scales and screening</span></button>
731 </div>
732 </div>`;}
733A.openAppt=id=>A.push('appt',{id});
734A.openPatient=id=>{DATA.recentSearch=[id,...DATA.recentSearch.filter(x=>x!==id)].slice(0,6);A.push('patient',{id});};
735
736/* ---------- Patients ---------- */
737function patientCard(p){return `<div class="pc"><button class="top" onclick="A.openPatient('${p.id}')">${avatar(p,58)}<span><div class="nm">${esc(p.name)}</div><span class="age">${p.age} yrs</span></span><span class="lv"><div class="l">Last consultation</div><div class="v">${p.lastVisit?relDays(p.lastVisit):'—'}</div></span></button>
738 <div class="bt"><button onclick="A.sheet('contact',{id:'${p.id}'})">${I('bubble')} Contact</button><button onclick="A.sheet('apptEditor',{pid:'${p.id}'})">${I('calplus')} Schedule</button></div></div>`;}
739function screenPatients(){const ps=[...DATA.patients].sort((a,b)=>(b.lastVisit||0)-(a.lastVisit||0));
740 return header({back:false,title:'Patients',trailing:[{icon:'search',label:'Search',on:"A.push('psearch')"},{icon:'plus',label:'Add Patient',on:"A.push('newPatient')"}]})+`<div class="content">${ps.length?ps.map(patientCard).join(''):`<div class="empty"><div class="t">No patients yet</div><div class="callout">Add a patient to start recording psychiatric evaluations, history and follow-up.</div><button class="btn s" onclick="A.push('newPatient')">Add Patient</button></div>`}</div>`;}
741A.setQuery=v=>{state.query=v;const el=document.getElementById('sresults');if(el)el.innerHTML=searchResults();};
742function searchResults(){const q=state.query.trim().toLowerCase();
743 if(!q){const rec=DATA.recentSearch.map(pat).filter(Boolean);return `<div class="sec"><div class="st" style="font-size:17px">Recent</div><span class="sp"></span>${rec.length?`<button class="quiet" onclick="DATA.recentSearch=[];A.setQuery('')">Clear all</button>`:''}</div>${rec.length?`<div class="group">${rec.map(prow).join('')}</div>`:`<div class="center cap3" style="padding:12px">Recently opened patients will appear here.</div>`}`;}
744 const res=DATA.patients.filter(p=>p.name.toLowerCase().includes(q)||p.phone.includes(q)||p.mrn.includes(q));
745 return res.length?`<div class="group">${res.map(prow).join('')}</div>`:`<div class="empty"><div class="t">No patients found</div><div class="callout">Try another name, phone number, or patient ID.</div><button class="btn s" style="width:auto;padding:0 16px" onclick="A.push('newPatient')">Add New Patient</button></div>`;}
746function prow(p){return `<button class="row" onclick="A.openPatient('${p.id}')">${avatar(p,42)}<span class="main"><div class="t1m">${esc(p.name)}</div><div class="cap">${p.age} yrs · ${esc(p.gender)}</div></span>${I('right','chev')}</button>`;}
747function screenSearch(){return `<div class="hdr"><div class="hrow"><button class="disc" onclick="state.query='';A.pop()" aria-label="Back">${I('left')}</button><label class="search" style="flex:1">${I('search')}<input id="psq" placeholder="Search by name or phone" value="${esc(state.query)}" oninput="A.setQuery(this.value)" autocomplete="off"></label></div></div><div class="content" id="sresults">${searchResults()}</div>`;}
748
749function screenPatient({id}){const p=pat(id);const evs=evalsFor(id);const latest=evs[0];const d=latest?latest.draft:null;
750 const risk=d&&d.clinicianRisk;const rt=riskTone(risk);
751 return header({title:esc(p.name),trailing:[{icon:'more',label:'More',on:`A.sheet('menu',{items:[['pencil','Edit patient',"A.closeSheet();A.push('newPatient',{edit:'${p.id}'})"],['trash','Delete patient',"A.sheet('confirm',{title:'Delete this patient?',msg:'Their evaluations and recordings are deleted with them. This cannot be undone.',ok:'Delete',on:'A.deletePatient(\\'${p.id}\\')'})",1]]})`}]})+`<div class="content">
752 <div style="display:flex;align-items:center;gap:12px">${avatar(p,52)}<div><div class="t1">${p.age} yrs · ${esc(p.gender)} · ${esc(p.phone)}</div><div class="callout">MRN ${p.mrn}</div></div></div>
753 <div class="card"><div style="display:flex;gap:12px;align-items:flex-start"><span class="tile" style="width:42px;height:42px;font-size:21px">${I('heart')}</span><div style="flex:1"><div class="ov">Clinical overview</div><div class="t1" style="margin-top:2px">${p.lastVisit?(sameDay(p.lastVisit,NOW)?'Seen today':'Last seen '+relDays(p.lastVisit).toLowerCase()):'Not yet seen'}</div>${p.lastVisit?`<div class="cap">${fmtDate(p.lastVisit)}</div>`:''}</div><div class="center"><div class="mono" style="color:var(--accent);font-size:17px;font-weight:600">${evs.length}</div><div class="cap">${evs.length===1?'evaluation':'evaluations'}</div></div></div>
754 ${latest?`<div style="border-top:1px solid var(--divider);padding-top:10px" class="cap">${I('turn')} Latest structured evaluation: ${fmtDate(latest.date)}</div>`:''}</div>
755 <button class="btn p" onclick="A.startEvalFor('${p.id}')">Start New Evaluation</button>
756 ${secHead('Patient details','Contact & record','idcard')}
757 <div class="card kvs"><div class="kv"><span class="k">Phone</span><span class="v" style="color:var(--accent)">${esc(p.phone)}</span></div><div class="kv"><span class="k">Email</span><span class="v ellip" style="color:var(--accent)">${esc(p.email||'—')}</span></div><div class="kv"><span class="k">Last visit</span><span class="v">${p.lastVisit?fmtDate(p.lastVisit):'—'}</span></div><div class="kv"><span class="k">Record created</span><span class="v">${fmtDate(p.created)}</span></div><div class="kv"><span class="k">Date of birth</span><span class="v">${p.dob.getDate()}/${p.dob.getMonth()+1}/${p.dob.getFullYear()}</span></div>${p.blood?`<div class="kv"><span class="k">Blood group</span><span class="v">${esc(p.blood)}</span></div>`:''}${p.emergency?`<div class="kv"><span class="k">Emergency contact</span><span class="v">${esc(p.emergency)}</span></div>`:''}${p.clinic?`<div class="kv"><span class="k">Clinic</span><span class="v">${esc(p.clinic)}</span></div>`:''}</div>
758 ${latest?secHead('Previous evaluation',fmtDateDow(latest.date),'doc')+`<div class="card">
759  <div><div class="lbl">Risk</div><div class="t1 ${rt==='dn'?'':''}" style="${rt==='dn'?'color:var(--danger)':''}">${esc(risk||'Not assessed')}</div></div>
760  ${(d.assessmentResults||[]).length?`<div style="display:flex;gap:18px">${d.assessmentResults.map(r=>`<div><div class="lbl">${r.title}</div><div class="mono t1">${r.score}/${r.max}</div></div>`).join('')}</div>`:''}
761  ${[['Mood',d.todayMood],['Sleep',d.todaySleep],['Functioning',d.todayFunction],['Working diagnosis',d.workingDiagnosis],['Follow-up',d.planFollowUp]].filter(x=>x[1]).map(([k,v])=>`<div><div class="lbl">${k}</div><div class="t1m">${esc(v)}</div></div>`).join('')}
762  <button class="btn s" onclick="A.push('saved',{id:'${latest.id}'})">View Full Evaluation</button></div>`:''}
763 ${secHead('Evaluation history',`${evs.length} evaluation${evs.length===1?'':'s'}`,'history')}
764 ${evs.length?`<div class="hist">${evs.map((e,i)=>`<button class="ev ${i===0?'latest':''}" onclick="A.push('saved',{id:'${e.id}'})"><div class="card"><div style="display:flex;justify-content:space-between;align-items:center"><span class="ov ${i===0?'ac':''}">${i===0?'Most recent':e.date.getFullYear()}</span>${I('right','chev')}</div><div class="t1">${esc(e.visitType)}</div><div class="cap">${fmtDateDow(e.date)}</div><div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px"><span class="tag ${riskTone(e.draft.clinicianRisk)}">Risk: ${esc(e.draft.clinicianRisk||'—')}</span><span class="cap">${scaleSummary(e.draft)}</span></div></div></button>`).join('')}</div>`:`<div class="card"><div class="empty"><div class="t">No evaluations yet</div><div class="callout">Evaluations for this patient appear here.</div></div></div>`}
765 </div>`;}
766A.deletePatient=id=>{DATA.patients=DATA.patients.filter(p=>p.id!==id);DATA.evaluations=DATA.evaluations.filter(e=>e.pid!==id);DATA.appointments=DATA.appointments.filter(a=>a.pid!==id);state.sheet=null;A.popTo('patients');A.toast('Patient deleted');};
767
768/* New patient form */
769const RELS=['Father','Mother','Spouse / partner','Son','Daughter','Brother','Sister','Friend','Caregiver','Other'];
770function screenNewPatient({edit}){const p=edit?pat(edit):null;const f=state.form||(state.form={name:p?p.name:'',dob:p?p.dob.toISOString().slice(0,10):'',gender:p?p.gender:'Female',code:'+91',phone:p?p.phone:'',email:p?p.email:'',ename:'',erel:'',ephone:'',blood:p?p.blood:'',clinic:p?p.clinic:'',more:false,edit});
771 const inp=(k,ph,type='text')=>`<div class="well"><input id="f_${k}" type="${type}" placeholder="${ph}" value="${esc(f[k])}" oninput="state.form.${k}=this.value;A.formCheck()">${type==='date'?I('cal'):''}</div>`;
772 const sel=(k,opts,ph)=>`<div class="well"><select id="f_${k}
772" onchange="state.form.${k}=this.value">${ph?`<option value="">${ph}</option>`:''}${opts.map(o=>`<option ${f[k]===o?'selected':''}>${o}</option>`).join('')}</select>${I('down')}</div>`;
773 const ok=f.name.trim()&&f.dob&&f.phone.trim();
774 return header({title:edit?'Edit Patient':'New Patient'})+`<div class="content">
775 <div class="fs"><div class="ov">Personal details</div>
776 <div><label class="fl" for="f_name">Full name <span class="req">*</span></label>${inp('name',"Patient's full name")}</div>
777 <div><label class="fl" for="f_dob">Date of birth <span class="req">*</span></label>${inp('dob','Select date','date')}</div>
778 <div><label class="fl" for="f_gender">Sex / Gender</label>${sel('gender',['Female','Male','Other','Prefer not to say'])}</div>
779 <div><label class="fl" for="f_phone">Mobile number <span class="req">*</span></label><div class="well"><select style="flex:none;width:64px" onchange="state.form.code=this.value">${['+91','+1','+44','+61','+65','+971'].map(c=>`<option ${f.code===c?'selected':''}>${c}</option>`).join('')}</select><span style="width:1px;height:24px;background:var(--divider)"></span><input id="f_phone" type="tel" placeholder="Mobile number" value="${esc(f.phone)}" oninput="state.form.phone=this.value;A.formCheck()"></div></div>
780 <div><label class="fl" for="f_email">Email</label>${inp('email','Optional','email')}</div></div>
781 <div class="fs"><div class="ov">Emergency / caregiver contact</div>
782 <div><label class="fl" for="f_ename">Name</label>${inp('ename','Optional')}</div>
783 <div><label class="fl" for="f_erel">Relationship</label>${sel('erel',RELS,'Select')}</div>
784 <div><label class="fl" for="f_ephone">Phone</label>${inp('ephone','Optional','tel')}</div></div>
785 <div class="coll"><button class="disc-row" onclick="state.form.more=!state.form.more;render()"><span>Additional details${f.more?'':'<div class="cap" style="font-weight:400">Blood group, clinic</div>'}</span>${I(f.more?'up':'down')}</button>${f.more?`<div class="body"><div><label class="fl" for="f_blood">Blood group</label>${sel('blood',['A+','A-','B+','B-','AB+','AB-','O+','O-'],'Select')}</div><div>
785<label class="fl" for="f_clinic">Clinic / hospital</label>${inp('clinic','Optional')}</div></div>`:''}</div>
786 <button class="btn p" id="savePatient" ${ok?'':'disabled'} onclick="A.savePatient()">${edit?'Save Changes':'Save Patient'}</button></div>`;}
787A.formCheck=()=>{const f=state.form;const b=document.getElementById('savePatient');if(b)b.disabled=!(f.name.trim()&&f.dob&&f.phone.trim());};
788A.savePatient=(force)=>{const f=state.form;if(!f.edit&&!force){const dup=DATA.patients.filter(p=>p.phone===f.phone.trim()||p.name.trim().toLowerCase()===f.name.trim().toLowerCase());if(dup.length){A.sheet('dup',{ids:dup.map(p=>p.id)});return;}}
789 const dob=new Date(f.dob);const age=Math.floor((NOW-dob)/(365.25*864e5));const em=f.ename?`${f.ename}${f.erel?' ('+f.erel+')':''}${f.ephone?' · '+f.code+' '+f.ephone:''}`:null;
790 if(f.edit){const p=pat(f.edit);Object.assign(p,{name:f.name.trim(),dob,age,gender:f.gender,phone:f.phone.trim(),email:f.email,emergency:em||p.emergency,blood:f.blood,clinic:f.clinic});state.form=null;state.sheet=null;A.pop();A.toast('Changes saved');return;}
791 const p=mkPatient({name:f.name.trim(),age,gender:f.gender,phone:f.phone.trim(),email:f.email,emergency:em,blood:f.blood,clinic:f.clinic,lastVisit:null,created:NOW,mrn:String(100000+Math.floor(Math.random()*900000)),hue:DATA.patients.length%8});p.dob=dob;DATA.patients.push(p);state.form=null;state.sheet=null;stack().pop();A.push('patient',{id:p.id});A.toast('Patient added');};
792
793/* ---------- Appointments ---------- */
794function apptRow(a){const p=pat(a.pid);const [t,ap]=fmtTime(a.at).split(' ');const st=apptDisplay(a);const tag=st==='Overdue'?'<span class="tag wn">Overdue</span>':st==='Completed'?'<span class="tag ok">Done</span>':st==='Cancelled'?'<span class="tag">Cancelled</span>':'';
795 return `<button class="row" onclick="A.openAppt('${a.id}')"><span style="width:72px;flex:none"><div class="mono t1m">${t} <span class="cap">${ap}</span></div><div class="cap ${a.urgent?'':''}" style="${a.urgent?'color:var(--danger)':''}">${a.urgent?'Urgent':dayLabel(a.at)}</div></span><span class="main"><div class="t1">${esc(a.name)}</div><div class="cap">${VISIT[a.type].short} · ${a.dur} min</div></span>${tag}${I('right','chev')}</button>`;}
796function screenAppts(){const td=todayAppts().filter(a=>a.status!=='Completed');const up=DATA.appointments.filter(a=>!sameDay(a.at,NOW)&&a.at>NOW).sort((a,b)=>a.at-b.at);const past=DATA.appointments.filter(a=>!sameDay(a.at,NOW)&&a.at<NOW).sort((a,b)=>b.at-a.at);
797 return header({back:false,title:'Appointments',trailing:[{icon:'plus',label:'Schedule appointment',on:"A.sheet('apptEditor',{})"}]})+`<div class="content">
798 ${td.length?secHead('Today',`${td.length} consultation${td.length===1?'':'s'}`,'cal')+`<div class="tlist">${td.map((a,i)=>tlRow(a,i,`A.openAppt('${a.id}')`)).join('')}</div>`:''}
799 ${up.length?secHead('Upcoming','','calclock')+`<div class="group">${up.map(apptRow).join('')}</div>`:''}
800 ${past.length?secHead('Past','','history')+`<div class="group">${past.map(apptRow).join('')}</div>`:''}
801 ${!td.length&&!up.length&&!past.length?`<div class="empty"><div class="t">No appointments</div><div class="callout">Schedule a consultation and it appears here.</div><button class="btn s" onclick="A.sheet('apptEditor',{})">Schedule appointment</button></div>`:''}
802 </div>`;}
803function countdown(a){const st=apptDisplay(a);if(st==='Completed'||st==='Cancelled')return [st,''];const ms=a.at-NOW;const end=apptEnd(a);
804 if(NOW>=a.at&&NOW<end)return [`In progress · ${Math.round((end-NOW)/6e4)} min left`,'ac'];
805 if(ms>0){const m=Math.round(ms/6e4);if(m<1)return ['Starting now','ac'];if(m<60)return [`Starts in ${m} min`,'ac'];if(sameDay(a.at,NOW))return [`Starts in ${Math.floor(m/60)} hr ${m%60} min`,''];return [dayLabelLong(a.at),''];}
806 const o=Math.round((NOW-end)/6e4);if(o<60)return [`Overdue by ${o} min`,'wn'];if(sameDay(a.at,NOW))return [`Overdue by ${Math.floor(o/60)} hr ${o%60} min`,'wn'];return ['Not marked completed','wn'];}
807function screenAppt({id}){const a=DATA.appointments.find(x=>x.id===id);if(!a)return header({title:'Appointment'})+'<div class="content"></div>';const p=pat(a.pid);const evs=evalsFor(a.pid);const last=evs[0];const d=last&&last.draft;const [cd,ct]=countdown(a);
808 const high=d&&(d.clinicianRisk==='High'||d.clinicianRisk==='Immediate concern');const evType=a.type==='initial'?'Initial Psychiatric Assessment':a.type==='crisis'?'Emergency / Crisis Assessment':'Follow-up Visit';
809 return header({title:esc(a.name),sub:VISIT[a.type].full,trailing:[{icon:'more',label:'More',on:`A.sheet('menu',{items:[['pencil','Edit appointment',"A.closeSheet();A.sheet('apptEditor',{id:'${a.id}'})"],['check','Mark completed',"A.apptStatus('${a.id}','Completed')"],['x','Mark cancelled',"A.apptStatus('${a.id}','Cancelled')"],['trash','Delete',"A.sheet('confirm',{title:'Delete this appointment?',msg:'The patient and their evaluations are not affected.',ok:'Delete',on:'A.deleteAppt(\\'${a.id}\\')'})",1]]})`}]})+`<div class="content">
810 <div class="card"><div style="display:flex;gap:12px;align-items:center"><span class="tile" style="--tint:${timeOfDayHue()};width:40px;height:40px;font-size:20px">${I('cal')}</span><div><div class="t1" style="font-size:18px">${fmtTime(a.at)} – ${fmtTime(apptEnd(a))}</div><div class="cap">${dayLabelLong(a.at)}</div></div></div><div style="display:flex;gap:8px;align-items:center">${a.urgent?'<span class="tag dn">Urgent</span>':''}<span class="tag ${ct}">${cd}</span><span class="sp" style="flex:1"></span><span class="cap3">${a.dur} min</span></div></div>
811 <button class="card" style="width:100%;text-align:left" onclick="A.openPatient('${p.id}')"><div style="display:flex;gap:12px;align-items:center">${avatar(p,48)}<div style="flex:1"><div class="t1">${esc(p.name)}</div><div class="cap">${p.age} · ${esc(p.gender)} · ${evs.length?`${evs.length} evaluation${evs.length===1?'':'s'}`:'No evaluations yet'}</div></div><span class="tile" style="width:36px;height:36px;border-radius:50%;font-size:16px;background:var(--accent-subtle)">${I('phone')}</span>${I('right','chev')}</div></button>
812 ${high?`<div class="safety">${I('warn')}<div><div class="t1m">Risk recorded at the last evaluation</div><div class="cap" style="color:inherit">${esc(d.clinicianRisk)}  ·  ${esc(d.suicidalThoughts||'')}</div></div></div>`:''}
813 <div><button class="btn p" onclick="A.startEvalFor('${p.id}','${evType}')">Start Evaluation</button><div class="center cap" style="margin-top:8px">Opens as ${evType}</div></div>
814 ${secHead('Last evaluation',last?`${esc(last.visitType)}  ·  ${fmtDate(last.date)}`:'','doc')}
815 <div class="card kvs">${last?[['Working diagnosis',d.workingDiagnosis],high?null:['Risk',d.clinicianRisk],['Medication',d.currentMedication],['Scales',(d.assessmentResults||[]).map(r=>`${r.title} ${r.score}/${r.max}`).join('  ·  ')],['Plan',d.planFollowUp]].filter(x=>x&&x[1]).map(([k,v])=>`<div class="kv"><span class="k">${k}</span><span class="v">${esc(v)}</span></div>`).join(''):'<div class="callout">First evaluation for this patient.</div>'}</div>
816 ${a.notes?secHead('Booking notes','','doc')+`<div class="card prose">${esc(a.notes)}</div>`:''}
817 ${secHead('Booking','','cal')}
818 <div class="card kvs"><div class="kv"><span class="k">Type</span><span class="v">${VISIT[a.type].full}</span></div><div class="kv"><span class="k">Status</span><span class="v">${a.status}</span></div><div class="kv"><span class="k">Clinician</span><span class="v">${esc(CLINICIAN.name)}</span></div><div class="kv"><span class="k">Booked</span><span class="v">${fmtDate(a.booked)} at ${fmtTime(a.booked)}</span></div></div>
819 </div>`;}
820A.apptStatus=(id,s)=>{const a=DATA.appointments.find(x=>x.id===id);a.status=s;state.sheet=null;render();A.toast(s==='Completed'?'Marked completed':'Marked cancelled');};
821A.deleteAppt=id=>{DATA.appointments=DATA.appointments.filter(a=>a.id!==id);state.sheet=null;A.pop();A.toast('Appointment deleted');};
822/* ---------- New Evaluation: chooser + type + entry ---------- */
823A.chooserQ=v=>{state.chooserQ=v;const el=document.getElementById('chooserList');if(el)el.innerHTML=chooserList();};
824function chooserList(){const q=state.chooserQ.trim().toLowerCase();let ps=[...DATA.patients].sort((a,b)=>(b.lastVisit||0)-(a.lastVisit||0));if(q)ps=ps.filter(p=>p.name.toLowerCase().includes(q)||p.phone.includes(q));else ps=ps.slice(0,6);
825 if(!ps.length)return `<div class="empty"><div class="t">No matches</div><div class="callout">No patient matches that search.</div></div>`;
826 return `<div class="ov">${q?'Patients':'Recent'}</div><div class="group" style="margin-top:12px">${ps.map(p=>`<button class="row" onclick="A.push('evalType',{id:'${p.id}'})">${avatar(p,40)}<span class="main"><div class="t1m ellip">${esc(p.name)}</div><div class="cap">${p.age} yrs · ${p.lastVisit?relDays(p.lastVisit):'Not yet seen'}</div></span>${I('right','chev')}</button>`).join('')}</div>`;}
827function screenChooser(){if(state.session)return screenWorkspace();
828 return header({back:false,title:'New Evaluation'})+`<div class="content">
829 ${state.pendingScale?`<div class="dnote">${I('info')}<div>A completed <b>${SCALES[state.pendingScale.instrument].title}</b> result (${state.pendingScale.score}/${state.pendingScale.max}) will be added to this evaluation.</div></div>`:''}
830 <div class="sec"><div class="st">Who is this evaluation for?</div></div>
831 <label class="search">${I('search')}<input placeholder="Search by name or phone" value="${esc(state.chooserQ)}" oninput="A.chooserQ(this.value)" autocomplete="off"></label>
832 <button class="row" style="background:var(--surface);border:1px solid var(--divider);border-radius:10px" onclick="A.push('newPatient')"><span class="tile" style="width:42px;height:42px;font-size:20px">${I('plus')}</span><span class="t1m" style="color:var(--accent)">New Patient</span></button>
833 <div id="chooserList">${chooserList()}</div></div>`;}
834function screenEvalType({id}){const p=pat(id);return header({title:'Evaluation Type'})+`<div class="content">
835 <div style="display:flex;align-items:center;gap:12px">${avatar(p,44)}<div><div class="t1m">${esc(p.name)}</div><div class="cap">MRN ${p.mrn}</div></div></div>
836 <div class="sec"><div class="st">What would you like to record?</div></div>
837 <div class="group">${EVAL_TYPES.map(([k,t,s,vt])=>`<button class="row" onclick="A.push('entry',{id:'${p.id}',vt:'${vt}'})"><span class="main"><div class="t1m">${t}</div><div class="cap">${s}</div></span>${I('right','chev')}</button>`).join('')}</div></div>`;}
838A.startEvalFor=(id,vt)=>{const p=pat(id);const prev=evalsFor(id)[0];A.push('entry',{id,vt:vt||(prev?'Follow-up Visit':'Initial Psychiatric Assessment')});};
839function prevCard(prev){if(!prev)return `<div class="card"><div style="display:flex;gap:10px;align-items:center">${I('spark')}<div><div class="t1m">No previous psychiatric assessment</div><div class="cap">Initial Psychiatric Assessment is suggested.</div></div></div></div>`;
840 const d=prev.draft;const kv=(k,v,cls='')=>v?`<div class="kv"><span class="k">${k}</span><span class="v ${cls}">${esc(v)}</span></div>`:'';
841 const box=(t,inner)=>inner.trim()?`<div class="prev"><div class="ov">${t}</div>${inner}</div>`:'';
842 const mse=[['Mood',d.mood],['Affect',d.affect],['Thought',d.thoughtProcess],['Perception',d.perception]].filter(x=>x[1]).map(x=>`${x[0]}: ${x[1]}`).join('; ');
843 return `<div class="card"><div style="display:flex;justify-content:space-between;align-items:center"><div><div class="t1" style="font-size:18px">Previous Visit</div><div class="cap">${esc(prev.visitType)}</div></div><span class="tag">${fmtDateShort(prev.date)}</span></div>
844 ${box('Safety',kv('Clinician risk',d.clinicianRisk,riskTone(d.clinicianRisk))+kv('Suicidal thoughts',d.suicidalThoughts))}
845 ${box('Status',kv('Mood',d.todayMood)+kv('Sleep',d.todaySleep)+kv('Function',d.todayFunction)+kv('PHQ-9',scoreOf(d,'phq9'))+kv('GAD-7',scoreOf(d,'gad7')))}
846 ${(d.selectedDomains||[]).length?box('Symptoms',`<div class="dchips">${d.selectedDomains.map(x=>`<span>${esc(x)}</span>`).join('')}</div>`):''}
847 ${d.presentingConcern?box('Presenting concern',`<div class="prose">${esc(d.presentingConcern)}</div>`):''}
848 ${mse?box('Mental state',`<div class="prose">${esc(mse)}</div>`):''}
849 ${box('Diagnosis & plan',kv('Working diagnosis',d.workingDiagnosis)+kv('Medication',d.currentMedication)+kv('Treatment',d.planPsychological)+kv('Follow-up',d.planFollowUp))}</div>`;}
850function screenEntry({id,vt}){const p=pat(id);const prev=evalsFor(id)[0];return evalHeader('New Evaluation')+`<div class="content nobar">
851 <div class="mini">${avatar(p,44)}<div><div class="cap">Patient</div><div class="t1m">${esc(p.name)}</div></div></div>
852 <button class="mini" onclick="A.sheet('picker',{label:'Consultation type',opts:['Initial Psychiatric Assessment','Follow-up Visit','Emergency / Crisis Assessment','Inpatient Review','Consult / Liaison','Quick Psychiatric Note'],value:'${vt}',noOther:true,on:'A.setEntryType'})"><span class="tile" style="width:36px;height:36px;border-radius:50%;font-size:18px">${I('steth')}</span><div style="flex:1;text-align:left"><div class="cap">Consultation type</div><div class="t1m">${vt}</div></div>${I('updown','ud')}</button>
853 ${prevCard(prev)}
854 </div><div class="entry-actions"><button class="btn p" onclick="A.startSession('${id}','${vt}')">Start Evaluation</button></div>`;}
855A.setEntryType=v=>{if(v)topS().vt=v;state.sheet=null;render();};
856
857/* ---------- session + workspace ---------- */
858const SECTIONS=['Safety','Symptoms','History','MSE','Scales','Formulation','Plan'];
859const HEADINGS={Safety:'Safety',Symptoms:'Symptoms',History:'History',MSE:'Mental Status Examination',Scales:'Scales',Formulation:'Clinical Formulation',Plan:'Assessment & Plan'};
860A.startSession=(pid,vt,editId)=>{const d=editId?clone(DATA.evaluations.find(e=>e.id===editId).draft):{selectedDomains:[],domainNotes:{},personalityTraits:[],assessmentResults:[]};
861 if(state.pendingScale&&!editId){d.assessmentResults=[state.pendingScale];state.pendingScale=null;}
862 state.session={pid,vt,draft:d,tab:'Safety',editId,rec:{status:editId?'stopped':'recording',seconds:0},transcript:editId?(DATA.evaluations.find(e=>e.id===editId).transcript||''):'',open:{}};
863 A.startTimer();
864 A.push('workspace');};
865const mmss=s=>`${String(Math.floor(s/60)).padStart(2,'0')}
865:${String(s%60).padStart(2,'0')}`;
866A.stopTimer=()=>{if(A._rt){clearInterval(A._rt);A._rt=null;}};
867A.startTimer=()=>{A.stopTimer();if(!state.session||state.session.rec.status!=='recording')return;A._rt=setInterval(()=>{const r=state.session&&state.session.rec;if(!r||r.status!=='recording')return; r.seconds++;document.querySelectorAll('[data-timer]').forEach(el=>el.textContent=mmss(r.seconds));},1000);};
868A.rec=cmd=>{const r=state.session.rec;if(cmd==='pause'){r.status='paused';A.stopTimer();}else if(cmd==='resume'){r.status='recording';A.startTimer();}else if(cmd==='stop'){r.status='stopped';A.stopTimer();if(!state.session.transcript)state.session.transcript=SAMPLE_TRANSCRIPT;}render();};
869function recBar(){const s=state.session;const r=s.rec;const st=r.status==='recording'?'Recording':r.status==='paused'?'Recording paused':'Recording saved';
870 const bts=r.status==='stopped'?`<button onclick="A.push('transcript')">Transcript</button>`:`<button onclick="A.rec('${r.status==='recording'?'pause':'resume'}')">${r.status==='recording'?'Pause':'Resume'}</button><button onclick="A.rec('stop')">Stop</button>`;
871 return `<div class="recbar"><span class="ic ${r.status==='recording'?'live':''}">${r.status==='recording'?I('mic'):r.status==='paused'?IF('pause'):I('doc')}</span><span class="main"><div class="st">${st}</div><div class="tm" data-timer>${mmss(r.seconds)}</div></span><span class="bts">${bts}</span></div>`;}
872function recIndicator(){const r=state.session.rec;if(r.status==='stopped')return '';return `<button class="recbar" style="width:100%" onclick="A.push('transcript')"><span class="ic ${r.status==='recording'?'live':''}">${r.status==='recording'?I('mic'):IF('pause')}</span><span class="main" style="display:flex;gap:8px;align-items:baseline"><span class="st">${r.status==='recording'?'Recording':'Recording paused'}</span><span class="tm">• <span data-timer>${mmss(r.seconds)}</span></span></span><span class="bts" style="color:var(--accent);align-items:center;gap:6px">${I('doc')} Transcript</span></button>`;}
873function miniHead(){const s=state.session;const p=pat(s.pid);return `<div class="mini">${avatar(p,34)}<div><div class="t1m">${esc(p.name)}</div><div class="cap">${esc(s.vt)}</div></div></div>`;}
874
875/* field helpers */
876const D=()=>state.session.draft;
877A.set=(k,v)=>{D()[k]=v;};
878A.setR=(k,v)=>{D()[k]=v;render();};
879A.pick=(k,label,listKey,noOther)=>A.sheet('picker',{label,opts:OPT[listKey]||listKey,value:D()[k]||'',noOther:!!noOther,on:`A.pickDone('${k}')`});
880A.pickDone=k=>v=>{D()[k]=v;state.sheet=null;render();};
881A.pickVal=v=>{const sh=state.sheet;const fn=sh.on.startsWith('A.pickDone')?A.pickDone(sh.on.match(/'(.*)'/)[1]):window[sh.on]||eval(sh.on);fn(v);};
882A.toggleMulti=(k,v)=>{const d=D();const a=d[k]||(d[k]=[]);const i=a.indexOf(v);if(i>=0)a.splice(i,1);else a.push(v);render();};
883function pk(k,label,listKey,noOther){const v=D()[k];return `<button class="pk" onclick="A.pick('${k}','${esc(label)}','${listKey}',${noOther?1:0})"><span class="main"><div class="l">${label}</div><div class="v ${v?'':'no'}">${esc(v||'Not assessed')}</div></span>${I('updown','ud')}</button>`;}
884const pkg=(...rows)=>`<div class="pkg">${rows.join('')}</div>`;
885function chips(k,label,opts){const v=D()[k];return `<div class="fld"><div class="lh"><span class="l">${label}</span>${v?`<button class="quiet" onclick="A.setR('${k}','')">Clear</button>`:''}</div><div class="chips ${opts.length===3?'eq':'wrap'}">${opts.map(o=>`<button class="chip ${v===o?'on':''}" onclick="A.setR('${k}','${esc(o)}')">${esc(o)}${v===o?I('check'):''}</button>`).join('')}</div></div>`;}
886function multi(k,opts,small){const a=D()[k]||[];return `<div class="chips wrap">${opts.map(o=>`<button class="chip ${small?'sm':''} ${a.includes(o)?'on':''}" onclick="A.toggleMulti('${k}','${esc(o)}')">${a.includes(o)?I('check'):''}${esc(o)}</button>`).join('')}</div>`;}
887function narr(k,label,ph,h=72){return `<div class="fld narr"><span class="l" style="font-size:13px;font-weight:600;color:var(--text2)">${label}</span><textarea id="n_${k}" style="min-height:${h}px" placeholder="${esc(ph)}" oninput="A.set('${k}',this.value)">${esc(D()[k]||'')}</textarea></div>`;}
888function ptitle(t){return `<div class="ptitle">${t}</div>`;}
889function cheading(t){return `<div class="ov">${t}</div>`;}
890function disclosure(key,title,inner,summary){const o=state.session.open[key];return `<div class="coll"><button class="disc-row" onclick="state.session.open['${key}']=!state.session.open['${key}'];render()"><span>${title}${summary?`<div class="cap" style="font-weight:400">${esc(summary)}</div>`:''}</span>${I(o?'up':'down')}</button>${o?`<div class="body">${inner}</div>`:''}</div>`;}
891
892/* section completeness + summaries */
893const cnt=(d,keys)=>keys.filter(k=>d[k]).length;
894const MSE10=['appearance','behavior','speech','mood','affect','thoughtProcess','thoughtContent','perception','orientation','insight'];
895function secState(name){const d=D();let complete=false,started=false;switch(name){
896 case 'Safety':complete=!!d.clinicianRisk;started=cnt(d,['suicidalThoughts','recentSelfHarm','harmToOthers','protectiveFactors','safetyNote'])>0;break;
897 case 'Symptoms':{const st=cnt(d,['todayMood','todaySleep','todayAppetite','todayEnergy','todayConcentration','todayFunction']);const dom=(d.selectedDomains||[]).some(x=>(d.domainNotes||{})[x]);complete=st>=3||dom;started=st>0||(d.selectedDomains||[]).length>0||!!d.presentingConcern;break;}
898 case 'History':{const n=cnt(d,['psychiatricHistory','medicationHistory','admissionsStatus','substanceUseStatus','fam
898ilyHistoryStatus','medicalHistory','socialHistory','traumaAdversity','currentMedication','adherence','response']);complete=n>=2;started=n>0;break;}
899 case 'MSE':{const n=cnt(d,MSE10);complete=n>=4;started=n>0||!!d.mseNotes;break;}
900 case 'Scales':complete=(d.assessmentResults||[]).length>0;break;
901 case 'Formulation':complete=!!d.formulation;started=cnt(d,['predisposing','precipitating','perpetuating','protective'])>0;break;
902 case 'Plan':{const n=cnt(d,['workingDiagnosis','differentials','planMedication','planPsychological','planInvestigations','planReferral','psychoeducationStatus','safetyPlanStatus','planFollowUp','planDisposition','planOther']);complete=n>=2;started=n>0;break;}}
903 return complete?'ok':started?'md':'no';}
904function secSummary(name){const d=D();switch(name){
905 case 'Safety':return d.suicidalThoughts||d.clinicianRisk?[d.suicidalThoughts,d.clinicianRisk?'Risk: '+d.clinicianRisk:''].filter(Boolean).join(' • '):'Not assessed';
906 case 'Symptoms':{const a=[d.todayMood?'Mood: '+d.todayMood:'',d.todaySleep?'Sleep: '+d.todaySleep:''].filter(Boolean);return a.length?a.join(' • '):((d.selectedDomains||[]).length?[...d.selectedDomains].sort().join(', '):'Record current status');}
907 case 'History':{const n=cnt(d,['psychiatricHistory','medicationHistory','admissionsStatus','substanceUseStatus','familyHistoryStatus','medicalHistory','socialHistory','traumaAdversity','currentMedication']);return n?`${n} area${n===1?'':'s'} recorded`:'Not explored';}
908 case 'MSE':{const a=[['Mood',d.mood],['Affect',d.affect],['Thought',d.thoughtProcess]].filter(x=>x[1]).map(x=>`${x[0]}: ${x[1]}`);return a.length?a.join(' • '):'Add / review';}
909 case 'Scales':return (d.assessmentResults||[]).length?d.assessmentResults.map(r=>`${r.title}: ${r.score}/${r.max}`).join(' • '):'Not completed';
910 case 'Formulation':return d.formulation?d.formulation.slice(0,70)+(d.formulation.length>70?'…':''):'Optional';
911 case 'Plan':{const a=[d.workingDiagnosis,d.planFollowUp,d.planDisposition].filter(Boolean);return a.length?a.join(' • '):'Add';}}}
912
913/* section bodies */
914function secSafety(){const d=D();const ide=['Passive','Active','Present'].includes(d.suicidalThoughts);
915 return pkg(pk('suicidalThoughts','Suicidal thoughts','suicidalThoughts'))+
916 (ide?`<div class="ideation"><div class="hd">${I('warn')} IDEATION DETAIL</div>${chips('passiveDeathWish','Passive death wish',OPT.dpu)}${chips('currentIntent','Current intent',OPT.dpu)}${chips('specificPlan','Specific plan',OPT.dpu)}${chips('accessMeans','Access to means',OPT.dpu)}${chips('previousAttempts','Previous attempts',OPT.dpu)}</div>`:'')+
917 pkg(pk('recentSelfHarm','Self-harm','recentSelfHarm'),pk('clinicianRisk','Overall clinician-assessed risk','clinicianRisk',true))+
918 chips('harmToOthers','Harm to others',OPT.dpu)+ptitle('Other acute concerns')+
919 chips('severeAgitation','Severe agitation',OPT.dpu)+chips('psychosisManiaRisk','Psychosis / mania immediate concern',OPT.dpu)+chips('unableCare','Unable to care for self',OPT.dpu)+chips('substanceIntoxicationConcern','Substance intoxication / withdrawal concern',OPT.dpu)+
920 narr('protectiveFactors','Protective factors','Enter what is holding the patient safe…',72)+narr('safetyNote','Safety notes','Enter clinical risk details…',84);}
921function secSymptoms(){const d=D();const doms=[...(d.selectedDomains||[])].sort();
922 return narr('presentingConcern','Presenting concern','Enter the presenting concern in the patient’s words…',72)+cheading('Current status')+
923 pkg(pk('todayMood','Mood','todayMood'),pk('todaySleep','Sleep','todaySleep'),pk('todayAppetite','Appetite','todayAppetite'),pk('todayEnergy','Energy','todayEnergy'),pk('todayConcentration','Concentration','todayConcentration'),pk('todayFunction','Function','todayFunction'))+
924 `<div class="sec"><div><div class="st" style="font-size:18px">Areas to explore</div><div class="sc">Select the symptom areas discussed today.</div></div></div>`+multi('selectedDomains',OPT.domains,true)+
925 doms.map(x=>`<div class="fld narr"><span class="l" style="font-size:13px;font-weight:600;color:var(--text2)">${esc(x)}</span><textarea style="min-height:72px" placeholder="Enter findings for ${esc(x.toLowerCase())}…" oninput="(D().domainNotes||(D().domainNotes={}))['${esc(x)}']=this.value">${esc((d.domainNotes||{})[x]||'')}</textarea></div>`).join('');}
926function secHistory(){const d=D();
927 return narr('psychiatricHistory','Past psychiatric history','Enter relevant psychiatric history…')+narr('medicationHistory','Previous treatment','Enter medication, therapy or prior treatment…')+
928 chips('admissionsStatus','Previous psychiatric admissions',OPT.admissions)+(d.admissionsStatus==='Yes'?narr('admissionDetail
928s','Admission details','Enter dates, setting and reason…'):'')+
929 chips('substanceUseStatus','Substance use',OPT.substance)+(['Current use','Past use'].includes(d.substanceUseStatus)?narr('substanceUse','Substance use details','Enter substances, quantity and frequency…'):'')+
930 chips('familyHistoryStatus','Family psychiatric history',OPT.family)+(d.familyHistoryStatus==='Present'?narr('familyHistory','Family history details','Enter relationship and condition…'):'')+
931 narr('medicalHistory','Relevant medical history','Enter relevant medical conditions…')+narr('socialHistory','Psychosocial history','Enter family, relationship, occupational, educational or social context…')+
932 disclosure('dev','Developmental History',`<div class="chips" style="flex-direction:column">${OPT.developmental.map(o=>`<button class="chip ${d.developmentalStatus===o?'on':''}" onclick="A.setR('developmentalStatus','${o}')">${o}${d.developmentalStatus===o?I('check'):''}</button>`).join('')}</div>${d.developmentalStatus==='Developmental concerns present'?narr('developmentalDetails','Developmental details','Enter milestones, school history, learning, social development or childhood concerns…'):''}`,d.developmentalStatus||'Not assessed')+
933 disclosure('pm','Premorbid Personality',`<div class="cap">Usual personality, relationships, coping style and functioning before the current illness.</div>${multi('personalityTraits',OPT.traits,true)}${(d.personalityTraits||[]).includes('Other')?narr('otherTraits','Other personality traits','Enter other longstanding traits…'):''}${narr('premorbidFunctioning','Premorbid personality / baseline functioning',"Enter the patient's usual personality and level of functioning…")}${narr('personalityNotes','Clinician notes','Add optional clinical notes…')}`,(d.personalityTraits||[]).length||d.premorbidFunctioning?'Recorded':'Not assessed')+
934 narr('traumaAdversity','Trauma / adversity','Enter relevant history…')+ptitle('Current treatment')+
935 pkg(pk('adherence','Medication adherence','adherence'),pk('response','Response to treatment','response'))+narr('currentMedication','Current medication','Enter current medication and doses…')+narr('sideEffects','Side effects','Enter reported side effects…');}
936function secMSE(){const d=D();
937 return pkg(pk('appearance','Appearance','appearance'),pk('behavior','Behaviour','behavior'),pk('speech','Speech','speech'),pk('mood','Mood','mood'),pk('affect','Affect','affect'),pk('thoughtProcess','Thought process','thoughtProcess'))+
938 pkg(pk('thoughtContent','Thought content','thoughtContent'))+(d.thoughtContent&&d.thoughtContent!=='No abnormal content reported'?narr('thoughtContentDetails','Thought content details','Enter findings…'):'')+
939 pkg(pk('perception','Perception','perception'))+(d.perception&&d.perception!=='No perceptual disturbance'?narr('perceptionDetails','Perception details','Enter findings…'):'')+
940 pkg(pk('orientation','Orientation','orientation'),pk('insight','Insight','insight'),pk('judgment','Judgment','judgment'))+
941 disclosure('mse','More MSE fields',pkg(pk('eyeContact','Eye contact','eyeContact'),pk('psychomotor','Psychomotor activity','psychomotor'),pk('cognition','Cognition','cognition'),pk('impulseControl','Impulse control','impulseControl')))+
942 narr('mseNotes','Additional MSE observations','Enter anything not captured above…');}
943function scaleCard(sid,r,prevScore,onclick){const s=SCALES[sid];return `<button class="scard ${r?'done':''}" onclick="${onclick}"><span class="hd">${s.title}${I('right')}</span><span class="cap">${s.full}</span>${r?`<span class="big">${r.score} / ${r.max}</span><span class="sev">${severity(sid,r.score)}</span><span class="cap">Completed today · <span class="lnk">View responses</span></span>`:'<span class="callout">Not completed</span>'}${prevScore?`<span class="cap">Previous visit: ${prevScore}</span>`:''}</button>`;}
944function secScales(){const d=D();const prev=evalsFor(state.session.pid).filter(e=>e.id!==state.session.editId)[0];
945 return `<div class="callout">The patient can answer these on this device. Scores are calculated from their responses.</div>`+SCALE_ORDER.map(sid=>{const r=(d.assessmentResults||[]).find(x=>x.instrument===sid);return scaleCard(sid,r,prev?scoreOf(prev.draft,sid):'',r?`A.sheet('scaleResult',{sid:'${sid}',retake:1})`:`A.runScale('${sid}','eval')`);}).join('');}
946function secFormulation(){return narr('formulation','Clinical formulation','Enter your clinical synthesis…',132)+disclosure('4p','Optional 4P helpers',narr('predisposing','Predisposing','Enter predisposing factors…',62)+narr('precipitating','Precipitating','Enter precipitating factors…',62)+narr('perpetuating','Perpetuating','Enter perpetuating factors…',62)+narr('protective','Protective','Enter protective factors…',62));}
947function secPlan(){const d=D();
948 return narr('workingDiagnosis','Working diagnosis','Enter working diagnosis…',62)+narr('differentials','Differential diagnoses','Enter differentials…',62)+narr('planMedication','Medication / treatment','Enter medication changes or treatment plan…')+narr('planPsychological','Psychological intervention','Enter psychological intervention…',62)+narr('planInvestigations','Investigations / referrals','Enter relevant investigations…',62)+narr('planReferral','Referral','Enter referrals…',62)+
949 cheading('Structured plan')+chips('psychoeducationStatus','Psychoeducation',OPT.psychoeducation)+(d.psychoeducationStatus==='Discussed'?narr('psychoeducationDetail
949s','Psychoeducation details','Enter topics discussed…'):'')+
950 chips('safetyPlanStatus','Safety plan',OPT.safetyPlan)+(['Reviewed','Updated','Created'].includes(d.safetyPlanStatus)?narr('planSafety','Safety plan details','Enter safety plan…'):'')+
951 pkg(pk('planFollowUp','Follow-up','followUp'),pk('planDisposition','Disposition','disposition'))+narr('planOther','Additional plan','Enter additional recommendations or next steps…');}
952const SEC_BODY={Safety:secSafety,Symptoms:secSymptoms,History:secHistory,MSE:secMSE,Scales:secScales,Formulation:secFormulation,Plan:secPlan};
953
954A.wsTab=t=>{state.session.tab=t;render(true);};
955A.wsNext=()=>{const i=SECTIONS.indexOf(state.session.tab);if(i<SECTIONS.length-1)A.wsTab(SECTIONS[i+1]);else A.push('review');};
956A.wsBack=()=>{const i=SECTIONS.indexOf(state.session.tab);if(i>0)A.wsTab(SECTIONS[i-1]);};
957A.fillSample=()=>{const s=state.session;const keep=s.draft.assessmentResults||[];s.draft=clone(SAMPLE_DRAFT);if(keep.length)s.draft.assessmentResults=keep;render();A.toast('Sample findings filled in');};
958A.leaveWorkspace=()=>{A.stopTimer();A.pop();};
959function screenWorkspace(){const s=state.session;const t=s.tab;const i=SECTIONS.indexOf(t);const g={ok:'✓',md:'•',no:'—'};
960 return evalHeader(s.editId?'Edit Evaluation':'New Evaluation','A.leaveWorkspace()',`<button class="disc" onclick="A.fillSample()" aria-label="Fill sample findings" title="Fill with sample findings">${I('spark')}</button>`)+
961 `<div class="content nobar" style="gap:16px">${recBar()}${miniHead()}
962 <div class="tabs"><button class="ar" onclick="A.wsBack()" ${i===0?'style="visibility:hidden"':''}>${I('left')}</button><div class="sc" id="tabsc">${SECTIONS.map(x=>{const st=secState(x);return `<button class="tb ${x===t?'on':''}" data-tab="${x}" onclick="A.wsTab('${x}')">${x} <span class="g ${st}">${g[st]}</span></button>`;}).join('')}</div><button class="ar" onclick="A.wsNext()" ${i===SECTIONS.length-1?'style="visibility:hidden"':''}>${I('right')}</button></div>
963 <div class="shead"><div class="st">${HEADINGS[t]}</div><div class="sc">${esc(secSummary(t))}</div></div>
964 ${SEC_BODY[t]()}
965 <div class="navbtns ${i===0?'one':''}">${i>0?`<button class="btn s" onclick="A.wsBack()">Back</button>`:''}<button class="btn p" onclick="A.wsNext()">${i===SECTIONS.length-1?'Continue':'Next'}</button></div>
966 <div class="fill-note">Tip: the ${I('spark')} button fills every section with sample findings.</div></div>`;}
967
968/* ---------- transcript ---------- */
969A.copyText=t=>{try{navigator.clipboard.writeText(t).then(()=>A.toast('Copied'),()=>A.toast('Select the text to copy it'));}catch(e){A.toast('Select the text to copy it');}};
970function screenTranscript(){const s=state.session;const ed=s.editingT;const has=!!s.transcript;
971 return `<div class="hdr"><div class="hrow"><button class="disc" onclick="A.pop()" aria-label="Back">${I('left')}</button><span class="sp"></span></div><div class="htitle">${s.editId?'Edit Evaluation':'New Evaluation'}</div></div><div class="content nobar">${miniHead()}
972 <div class="card"><div style="display:flex;justify-content:space-between;align-items:center"><span class="t1" style="font-size:20px">Transcript</span><span style="display:flex;gap:14px">${ed?`<button class="quiet" onclick="state.session.editingT=false;render()">Cancel</button><button class="quiet" onclick="state.session.transcript=document.getElementById('tEdit').value;state.session.editingT=false;render();A.toast('Transcript saved')">Save</button>`:`<button class="quiet" onclick="A.copyText(state.session.transcript)">${I('copy')} Copy</button><button class="quiet" ${has?'':'disabled'} onclick="state.session.editingT=true;render()">${I('pencil')} Edit</button><button class="quiet" onclick="A.pop()">Close</button>`}</span></div>
973 <div class="transcript">${ed?`<textarea id="tEdit">${esc(s.transcript)}</textarea>`:(has?esc(s.transcript):'<span class="cap3">No transcript captured yet.</span>'+(s.rec.status!=='stopped'?'<div class="cap" style="margin-top:8px">The transcript is prepared on-device once you stop the recording.</div>':''))}</div>
974 <div class="cap">
974Transcript is what was said. Evaluation fields are clinician-confirmed findings.</div></div></div>`;}
975
976/* ---------- report renderer (review + saved) ---------- */
977function report(d,o){const kv=(k,v,cls='')=>v?`<div class="kv"><span class="k">${k}</span><span class="v ${cls}">${esc(v)}</span></div>`:'';
978 const prose=(l,v)=>v?`<div>${l?`<div class="lbl" style="margin-bottom:3px">${l}</div>`:''}<div class="prose">${esc(v)}</div></div>`:'';
979 const card=(key,title,editTab,inner,collapsedCount)=>{if(!inner.trim())return '';const open=o.open[key]!==undefined?o.open[key]:!collapsedCount;return `<div class="card rcard"><div class="hd"><span class="t">${title}</span><span style="display:flex;gap:12px;align-items:center">${o.edit&&editTab?`<button class="quiet" onclick="A.reviewEdit('${editTab}')">Edit</button>`:''}${collapsedCount?`<button class="quiet" style="color:var(--text3)" onclick="A.toggleR('${key}',${open?0:1})">${I(open?'up':'down')}</button>`:''}</span></div>${open?inner:`<div class="cap">${collapsedCount} recorded item${collapsedCount===1?'':'s'}</div>`}</div>`;};
980 const kvs=rows=>{const h=rows.map(r=>kv(...r)).join('');return h?`<div class="kvs">${h}</div>`:'';};
981 const rt=riskTone(d.clinicianRisk);
982 const summary=card('sum','Clinical Summary','Symptoms',prose('',d.presentingConcern)+prose('Interval history',d.intervalNote));
983 const safety=card('saf','Safety & Risk','Safety',kvs([['Overall clinician-assessed risk',d.clinicianRisk,rt],['Suicidal thoughts',d.suicidalThoughts],['Self-harm',d.recentSelfHarm],['Harm to others',d.harmToOthers],['Severe agitation',d.severeAgitation],['Psychosis / mania immediate concern',d.psychosisManiaRisk],['Unable to care for self',d.unableCare],['Substance intoxication / withdrawal concern',d.substanceIntoxicationConcern]])+kvs([['Passive death wish',d.passiveDeathWish],['Current intent',d.currentIntent],['Specific plan',d.specificPlan],['Access to means',d.accessMeans],['Previous attempts',d.previousAttempts],['Protective factors',d.protectiveFactors]])+prose('Clinical risk note',d.safetyNote));
984 let since='';if(o.prev){const p=o.prev;const t=(l,a,b)=>(a||b)?`<div class="c"><div class="l">${l}</div><div class="v">${esc(a||'—')} → <b>${esc(b||'—')}</b></div></div>`:'';since=card('since','Since Previous Visit','Symptoms',`<div class="cmp">${t('Mood',p.todayMood,d.todayMood)+t('Sleep',p.todaySleep,d.todaySleep)+t('Risk',p.clinicianRisk,d.clinicianRisk)+t('Function',p.todayFunction,d.todayFunction)+t('PHQ-9',scoreOf(p,'phq9'),scoreOf(d,'phq9'))+t('GAD-7',scoreOf(p,'gad7'),scoreOf(d,'gad7'))}</div>`);}
985 const doms=(d.selectedDomains||[]);const notes=doms.filter(x=>(d.domainNotes||{})[x]);
986 const symptoms=card('sym','Symptoms','Symptoms',(o.foldSummary?prose('',d.presentingConcern)+prose('Interval history',d.intervalNote):'')+kvs([['Mood',d.todayMood],['Sleep',d.todaySleep],['Appetite',d.todayAppetite],['Energy',d.todayEnergy],['Concentration',d.todayConcentration],['Function',d.todayFunction]])+(doms.length?`<div class="dchips">${doms.map(x=>`<span>${esc(x)}</span>`).join('')}</div>`:'')+notes.map(x=>prose(x,d.domainNotes[x])).join(''));
987 const mse=card('mse','Mental Status Examination','MSE',kvs([['Appearance',d.appearance],['Behaviour',d.behavior],['Eye contact',d.eyeContact],['Psychomotor activity',d.psychomotor],['Speech',d.speech],['Mood',d.mood],['Affect',d.affect],['Thought process',d.thoughtProcess],['Thought content',d.thoughtContent],['Thought content details',d.thoughtContentDetails],['Perception',d.perception],['Perception details',d.perceptionDetails],['Cognition',d.cognition],['Orientation',d.orientation],['Insight',d.insight],['Judgment',d.judgment],['Impulse control',d.impulseControl]])+prose('Additional observations',d.mseNotes));
988 const scales=(d.assessmentResults||[]).length?card('sc','Scales','Scales',d.assessmentResults.map(r=>`<button class="scard done" onclick="A.sheet('scaleResult',{sid:'${r.instrument}',result:${o.evalId?`'${o.evalId}'`:'null'}})"><span class="hd">${r.title}${I('right')}</span><span class="big">${r.score} / ${r.max}</span><span class="sev">${severity(r.instrument,r.score)}</span><span class="cap">Completed ${o.date?fmtDate(o.date):'today'} · <span class="lnk">View responses</span></span></button>`).join('')):'';
989 const hist=[['Past psychiatric history',d.psychiatricHistory],['Previous treatment',d.medicationHistory],['Previous psychiatric admissions',d.admissionsStatus],['Admission details',d.admissionsStatus==='Yes'?d.admissionDetail
989s:''],['Substance use',d.substanceUseStatus],['Substance use details',['Current use','Past use'].includes(d.substanceUseStatus)?d.substanceUse:''],['Family psychiatric history',d.familyHistoryStatus],['Family history details',d.familyHistoryStatus==='Present'?d.familyHistory:''],['Relevant medical history',d.medicalHistory],['Psychosocial history',d.socialHistory],['Developmental History',d.developmentalStatus==='No concerns reported'?'Development reportedly within normal limits. No significant developmental concerns reported.':d.developmentalDetails],['Premorbid Personality',[(d.personalityTraits||[]).join(', '),d.otherTraits,d.premorbidFunctioning,d.personalityNotes].filter(Boolean).join('. ')],['Trauma / adversity',d.traumaAdversity]].filter(x=>x[1]);
990 const history=card('hist','Relevant History','History',kvs(hist),hist.length);
991 const med=[['Current medication',d.currentMedication],['Adherence',d.adherence],['Response',d.response],['Side effects',d.sideEffects],['Psychological treatment',d.psychologicalTreatment]].filter(x=>x[1]);
992 const meds=card('med','Medication & Treatment','History',kvs(med),med.length);
993 const form=card('form','Formulation','Formulation',prose('',d.formulation)+kvs([['Predisposing',d.predisposing],['Precipitating',d.precipitating],['Perpetuating',d.perpetuating],['Protective',d.protective]]));
994 const plan=card('plan','Plan & Follow-up','Plan',kvs([['Working diagnosis',d.workingDiagnosis],['Differential diagnoses',d.differentials],['Medication / treatment',d.planMedication],['Psychological intervention',d.planPsychological],['Investigations',d.planInvestigations],['Referral',d.planReferral],['Psychoeducation',d.psychoeducationStatus],['Psychoeducation details',d.psychoeducationStatus==='Discussed'?d.psychoeducationDetails:''],['Safety plan',d.safetyPlanStatus],['Safety plan details',['Reviewed','Updated','Created'].includes(d.safetyPlanStatus)?d.planSafety:''],['Follow-up',d.planFollowUp],['Disposition',d.planDisposition],['Additional plan',d.planOther]]));
995 const notesC=o.foldSummary?'':card('notes','Additional Notes','',prose('',d.additionalClinicalNote),d.additionalClinicalNote?1:0);
996 const all=o.foldSummary?[safety,symptoms,mse,scales,history,meds,form,plan]:[summary,safety,since,symptoms,mse,scales,history,meds,form,plan,notesC];
997 const body=all.join('');
998 return body.trim()?body:`<div class="card"><div class="empty"><div class="t">No structured findings recorded</div><div class="callout">You can still save audio, attachments, or a free-text note.</div></div></div>`;}
999function overview(d,p,title,dateStr){const rt=riskTone(d.clinicianRisk);const ic=rt==='ok'?'shieldck':rt==='nt'?'shield':'warn';
1000 return `<div class="card"><div class="t1" style="font-size:22px">${title}</div><div style="display:flex;gap:10px;align-items:center">${avatar(p,38)}<div><div class="t1">${esc(p.name)}</div><div class="cap">${dateStr}</div></div></div>
1001 ${d.presentingConcern&&title==='Saved Evaluation'?`<div class="prose">${esc(d.presentingConcern)}</div>`:''}
1002 ${(d.assessmentResults||[]).map(r=>`<div class="scstrip"><b>${r.title}</b><span class="mono">${r.score}/${r.max}</span><span class="sp">${severity(r.instrument,r.score)}</span></div>`).join('')}
1003 ${d.clinicianRisk?`<div class="risk ${rt}">${I(ic)} Clinician-assessed risk · ${esc(d.clinicianRisk)}</div>`:''}</div>`;}
1004A.toggleR=(k,v)=>{(state.rOpen||(state.rOpen={}))[k]=!!v;render();};
1005A.reviewEdit=t=>{state.session.tab=t;A.pop();};
1006function screenReview(){const s=state.session;const p=pat(s.pid);const d=s.draft;const prev=evalsFor(s.pid).filter(e=>e.id!==s.editId)[0];const r=s.rec;
1007 const primary=r.status==='recording'||r.status==='paused'?`<button class="btn p" onclick="A.rec('stop');A.toast('Recording saved')">Stop Recording & Prepare Note</button>`:`<button class="btn p" onclick="A.saveNote()">Save Note</button>`;
1008 return `<div class="hdr"><div class="hrow"><button class="disc" onclick="state.session.tab='Plan';A.pop()" aria-label="Back">${I('left')}</button><span class="sp"></span></div><div class="htitle">${s.editId?'Edit Evaluation':'New Evaluation'}</div></div><div class="content nobar">${recIndicator()}
1009 ${overview(d,p,'Evaluation Review',`${fmtDateUS(NOW)} · ${esc(s.vt)}`)}
1010 ${report(d,{edit:true,open:state.rOpen||{},prev:prev&&prev.draft})}
1011 ${s.transcript?`<button class="card" style="width:100%;text-align:left" onclick="A.push('transcript')"><div style="display:flex;gap:12px;align-items:center"><span class="tile" style="width:40px;height:40px;border-radius:50%;font-size:20px">${I('wave')}</span><div style="flex:1"><div class="t1">Transcript</div><div class="cap">
10111 recording · ${r.seconds<60?'under a minute':Math.round(r.seconds/60)+' min'}</div></div>${I('right','chev')}</div></button>`:''}
1012 ${primary}<button class="btn d" onclick="A.sheet('confirm',{title:'Discard this evaluation?',msg:'The evaluation, recording, generated note text, and attached media will be permanently removed from this draft.',ok:'Delete',on:'A.discardEval()'})">Discard Evaluation</button></div>`;}
1013A.discardEval=()=>{state.sheet=null;A.stopTimer();state.session=null;state.rOpen=null;A.popTo(stack()[0].s);A.toast('Evaluation discarded');};
1014A.saveNote=()=>{const s=state.session;const p=pat(s.pid);let ev;if(s.editId){ev=DATA.evaluations.find(e=>e.id===s.editId);ev.draft=s.draft;ev.transcript=s.transcript||ev.transcript;}else{ev={id:nid(),pid:s.pid,date:new Date(NOW),visitType:s.vt,draft:s.draft,transcript:s.transcript,seconds:s.rec.seconds};DATA.evaluations.push(ev);p.lastVisit=new Date(NOW);}
1015 A.stopTimer();state.session=null;state.rOpen=null;const st=stack();st.length=1;if(state.tab==='neweval'){state.tab='patients';state.stacks.patients=[{s:'patients'},{s:'patient',id:p.id}];}else if(st[0].s==='patients'||st[0].s==='home'||st[0].s==='appts'){if(state.tab==='patients')st.push({s:'patient',id:p.id});}
1016 A.push('saved',{id:ev.id});A.toast('Evaluation saved');};
1017
1018/* ---------- saved evaluation ---------- */
1019function screenSaved({id}){const ev=DATA.evaluations.find(e=>e.id===id);if(!ev)return header({title:'Evaluation'})+`<div class="content"><div class="empty"><div class="t">Evaluation unavailable</div><div class="callout">This evaluation could not be opened. It may have been deleted.</div></div></div>`;
1020 const p=pat(ev.pid);const d=ev.draft;const dur=ev.seconds?`${Math.floor(ev.seconds/60)}:${String(ev.seconds%60).padStart(2,'0')}`:'--:--';
1021 return header({title:'Psychiatric Evaluation',sub:esc(p.name)})+`<div class="content">
1022 <div class="two"><button class="btn s" onclick="A.sheet('export',{id:'${ev.id}'})">Export</button><button class="btn s" onclick="A.startSession('${p.id}','${esc(ev.visitType)}','${ev.id}')">Edit Evaluation</button></div>
1023 ${overview(d,p,'Saved Evaluation',`${fmtDate(ev.date)} · ${esc(ev.visitType)}`)}
1024 ${report(d,{edit:false,open:state.rOpen||{},foldSummary:true,evalId:ev.id,date:ev.date})}
1025 ${ev.seconds?`<div class="card"><div class="ov">Recording</div><div class="player"><button class="pb" onclick="A.toast('Audio playback is not part of this demo')">${IF('play')}</button><div style="flex:1"><div class="t1m">Recording</div><div class="cap">${fmtDateShort(ev.date)} • ${fmtTime(ev.date)}</div></div><span class="mono cap">${dur}</span></div><div class="overlay-audio"><span>0:00</span><div class="slider" style="--p:0%"></div><span>${dur}</span></div>${ev.transcript?`<button class="quiet" onclick="A.sheet('savedTranscript',{id:'${ev.id}'})">${I('doc')} View Transcript</button>`:'<span class="cap3">Transcript unavailable</span>'}</div>`:''}
1026 ${d.additionalClinicalNote?`<div class="card"><div class="ov">Additional clinical note</div><div class="prose">${esc(d.additionalClinicalNote)}</div></div>`:''}
1027 </div>`;}
1028/* ---------- Tools ---------- */
1029function screenTools(){return header({title:'Tools'})+`<div class="content">
1030 ${state.pendingScale?`<div class="card"><div class="t1" style="font-size:18px">A completed scale is waiting</div><div class="callout">It will be added to the next evaluation you start.</div><button class="quiet dn" onclick="state.pendingScale=null;render()">Discard it</button></div>`:''}
1031 ${SCALE_ORDER.map(sid=>{const s=SCALES[sid];return `<div class="card"><button class="tool" style="--tint:${s.tint}" onclick="A.runScale('${sid}','tools')"><span class="tile">${I(s.icon)}</span><span style="flex:1;min-width:0"><div class="tt">${s.title}</div><div class="callout">${s.desc}</div><div class="mc"><span style="--tint:var(--accent)">${s.q.length} questions</span><span style="--tint:var(--accent)">~2 min</span><span>${s.domain}</span></div></span>${I('right','chev')}</button></div>`;}).join('')}</div>`;}
1032
1033/* ---------- scale questionnaire ---------- */
1034A.runScale=(sid,ctx)=>{const s=SCALES[sid];state.scaleRun={sid,ctx,idx:0,answers:{},gate:s.gate?null:undefined,phase:s.gate?'gate':'q'};A.push('scale');};
1035A.ans=(i,v)=>{const r=state.scaleRun;r.answers[i]=v;render();};
1036A.gateAns=v=>{state.scaleRun.gate=v;render();};
1037A.scaleNext=()=>{const r=state.scaleRun;const s=SCALES[r.sid];if(r.phase==='gate'){if(r.gate===0){r.answers={};r.phase='result';}else r.phase='q';render();return;}
1038 if(r.idx<s.q.length-1)r.idx++;else r.phase='result';render();};
1039A.scaleBack=()=>{const r=state.scaleRun;if(r.phase==='q'&&r.idx>0)r.idx--;else if(r.phase==='q'&&SCALES[r.sid].gate)r.phase='gate';render();};
1040A.scaleExit=()=>{const r=state.scaleRun;
1040if(Object.keys(r.answers).length&&r.phase!=='result'){A.sheet('confirm',{title:'Discard entered responses?',msg:'Your current responses have not been saved.',ok:'Discard',cancel:'Keep Editing',on:'state.sheet=null;state.scaleRun=null;A.pop()'});return;}state.scaleRun=null;A.pop();};
1041function scaleResultObj(r){const s=SCALES[r.sid];const score=Object.values(r.answers).reduce((a,b)=>a+b,0);return {instrument:r.sid,title:s.title,score,max:s.max,responses:clone(r.answers),trauma:s.gate?r.gate===1:undefined};}
1042A.scaleFinish=()=>{const r=state.scaleRun;const res=scaleResultObj(r);if(r.ctx==='eval'){const d=D();d.assessmentResults=(d.assessmentResults||[]).filter(x=>x.instrument!==r.sid);d.assessmentResults.push(res);state.scaleRun=null;A.pop();A.toast(`${res.title} added to evaluation`);}
1043 else{state.pendingScale=res;state.scaleRun=null;stack().pop();state.tab='neweval';state.stacks.neweval=[{s:'chooser'}];render(true);}};
1044A.scaleRestart=()=>{const r=state.scaleRun;r.answers={};r.idx=0;r.phase=SCALES[r.sid].gate?'gate':'q';r.gate=SCALES[r.sid].gate?null:undefined;state.sheet=null;render();};
1045function responsesList(sid,res){const s=SCALES[sid];if(s.gate&&res.trauma===false)return `<div class="card callout">Trauma exposure was not endorsed, so the symptom items were not asked.</div>`;
1046 return `<div class="lbl" style="font-weight:600">Responses</div>${s.q.map((q,i)=>{const v=res.responses[i+1];const lab=(s.opts.find(o=>o[1]===v)||[''])[0];return `<div class="resp"><div class="q">${i+1}. ${esc(q)}</div><div class="a">${s.gate?lab:`${lab} — ${v}`}</div></div>`;}).join('')}`;}
1047function screenScale(){const r=state.scaleRun;const s=SCALES[r.sid];
1048 if(r.phase==='result'){const res=scaleResultObj(r);const evalCtx=r.ctx==='eval';
1049  return header({title:s.title,sub:s.gate?s.full:'Result'})+`<div class="content nobar" style="gap:18px">
1050  <div><div class="t1" style="font-size:22px">${s.title} Result</div>${s.gate?`<div class="cap">Trauma exposure endorsed: ${r.gate===1?'Yes':'No'}</div>`:''}<div class="cap">${NOW.getDate()} ${DAYS[NOW.getDay()].slice(0,0)}${['January','February','March','April','May','June','July','August','September','October','November','December'][NOW.getMonth()]} ${NOW.getFullYear()}, ${fmtTime(NOW)}</div>${state.session?`<div class="cap">Patient: ${esc(pat(state.session.pid).name)}</div>`:''}</div>
1051  <div class="card"><div class="lbl" style="font-weight:600">Score</div><div class="score">${res.score} / ${res.max}</div>${evalCtx||s.gate?`<div class="lbl" style="font-weight:600">Severity</div><div class="t1m" style="display:flex;gap:6px;align-items:center;color:var(--accent)">${I('info')}<span style="color:var(--text)">${severity(r.sid,res.score)}</span></div>`:''}</div>
1052  ${evalCtx||s.gate?responsesList(r.sid,res):''}
1053  <div style="display:flex;flex-direction:column;gap:12px">${evalCtx?`<button class="tbtn p" onclick="A.scaleFinish()">Add to Evaluation</button><button class="tbtn s" onclick="state.scaleRun.phase='${s.gate?'gate':'q'}';state.scaleRun.idx=0;render()">Edit Responses</button><button class="tbtn s" onclick="state.scaleRun=null;A.pop()">Cancel</button>`:`<button class="tbtn p" onclick="A.scaleFinish()">Start Evaluation with Result</button><button class="tbtn s" onclick="A.sheet('confirm',{title:'${s.gate?'Retake assessment?':'Start over?'}',msg:'This will clear your current responses.',ok:'${s.gate?'Retake Assessment':'Start Over'}',on:'A.scaleRestart()'})">${s.gate?'Retake Assessment':'Start Over'}</button><button class="tbtn s" onclick="state.scaleRun=null;A.pop()">Done</button>`}</div></div>`;}
1054 if(r.phase==='gate'){return header({title:s.title,sub:s.full})+`<div class="content nobar">
1055  <div class="qcard"><div class="tf">Lifetime</div><div class="q">${esc(s.gateQ)}</div></div>
1056  <div class="opts">${[['No',0],['Yes',1]].map(([l,v])=>`<button class="opt ${r.gate===v?'on':''}" onclick="A.gateAns(${v})"><span>${l}</span>${r.gate===v?I('checkc'):I('info').replace(ICONS.info,'<circle cx="12" cy="12" r="9"/>')}</button>`).join('')}</div>
1057  <div class="two"><button class="tbtn s" disabled style="opacity:.5">Back</button><button class="tbtn p" ${r.gate==null?'disabled':''} onclick="A.scaleNext()">${r.gate===0?'View Result':'Next'}</button></div></div>`;}
1058 const i=r.idx,n=s.q.length,v=r.answers[i+1];
1059 return `<div class="hdr"><div class="hrow"><button class="disc" onclick="A.scaleExit()" aria-label="Back">${I('left')}</button><span class="sp"></span></div><div class="htitle">${s.title}</div><div class="hsub">${s.full}</div></div><div class="content nobar">
1060 <div class="prog"><div class="t">Question ${i+1} of ${n}</div><div class="bar"><i style="width:${(i+1)/n*100}%"></i></div></div>
1061 <div class="qcard"><div class="tf">${esc(s.gate?'Over the past month':s.time)}</div><div class="q">${esc(s.q[i])}</div></div>
1062 <div class="opts">${s.opts.map(([l,val])=>`<button class="opt ${v===val?'on':''}" onclick="A.ans(${i+1},${val})"><span>${l}</span>${v===val?I('checkc'):'<svg class="i" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg>'}</button>`).join('')}</div>
1063 <div class="two"><button class="tbtn s" ${i===0&&!s.gate?'disabled style="opacity:.5"':''} onclick="A.scaleBack()">Back</button><button class="tbtn p" ${v==null?'disabled':''} onclick="A.scaleNext()">${i===n-1?'View Result':'Next'}</button></div></div>`;}
1064
1065/* ---------- Settings (not part of this demo) ---------- */
1066function screenSettings(){return header({back:false,title:'Settings'})+`<div class="content"><div class="card"><div style="display:flex;gap:12px;align-items:center">${avatar({name:CLINICIAN.name.replace('Dr. ',''),hue:5},44)}<div><div class="t1">${esc(CLINICIAN.name)}</div><div class="cap">${esc(CLINICIAN.clinic)}</div></div></div></div><div class="dnote">${I('info')}<div>Profile, backup, export and audit-log settings are not part of this interactive demo.</div></div></div>`;}
1067
1068/* ---------- sheets ---------- */
1069function sheetHTML(){const sh=state.sheet;if(!sh)return '';let body='',head='',foot='';
1070 switch(sh.type){
1071 case 'picker':{const custom=sh.value&&!sh.opts.includes(sh.value);head=`<button class="quiet" onclick="A.closeSheet()">Cancel</button><span class="t">${esc(sh.label)}</span><span style="width:48px"></span>`;
1072  body=`<div class="group">${sh.opts.map(o=>`<button class="opt-row ${sh.value===o?'on':''}" onclick="A.pickVal('${esc(sh.value===o?'':o)}')">${sh.value===o?I('checkc'):'<svg class="i" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/></svg>'}<span>${esc(o)}</span></button>`).join('')}${custom?`<button class="opt-row on" onclick="A.pickVal('')">${I('checkc')}<span>${esc(sh.value)}<div class="cap">Custom</div></span></button>`:''}</div>
1073  ${sh.noOther?'':`<div class="group">${sh.other?`<div style="padding:12px 16px" class="fs"><label class="fl" for="otherF">Specify ${esc(sh.label.toLowerCase())}</label><div class="well"><input id="otherF" placeholder="Finding" value="${esc(sh.value||'')}"></div><div class="two"><button class="btn s" onclick="state.sheet.other=false;render()">Cancel</button><button class="btn p" onclick="A.pickVal(document.getElementById('otherF').value.trim())">Save</button></div></div>`:`<button class="opt-row" onclick="state.sheet.other=true;render()">${I('sqpen')}<span>Other…</span></button>`}<button class="opt-row dn" ${sh.value?'':'disabled'} onclick="A.pickVal('')">${I('x')}<span>Not assessed</span></button></div>`}`;break;}
1074 case 'menu':body=`<div class="group">${sh.items.map(([ic,l,on,dn])=>`<button class="opt-row ${dn?'dn':''}" onclick="${on}">${I(ic)}<span>${l}</span></button>`).join('')}</div><button class="btn s" onclick="A.closeSheet()">Cancel</button>`;break;
1075 case 'confirm':body=`<div class="center" style="padding:8px 0"><div class="t1" style="font-size:18px">${esc(sh.title)}</div><div class="callout" style="margin-top:6px">${esc(sh.msg)}</div></div><button class="btn p" style="background:var(--danger)" onclick="${sh.on}">${esc(sh.ok)}</button><button class="btn s" onclick="A.closeSheet()">${esc(sh.cancel||'Cancel')}</button>`;break;
1076 case 'contact':{const p=pat(sh.id);head=`<span class="t">Contact ${esc(p.name)}</span><button class="quiet" onclick="A.closeSheet()">Done</button>`;body=`<div class="group"><button class="row" onclick="A.copyText('${esc(p.phone)}')"><span class="tile" style="width:36px;height:36px;border-radius:50%;font-size:16px">${I('phone')}</span><span class="main"><div class="t1m">${esc(p.phone)}</div><div class="cap">Mobile · tap to copy</div></span></button>${p.email?`<button class="row" onclick="A.copyText('${esc(p.email)}')"><span class="tile" style="width:36px;height:36px;border-radius:50%;font-size:16px">${I('bubble')}</span><span class="main"><div class="t1m ellip">${esc(p.email)}</div><div class="cap">Email · tap to copy</div></span></button>`:''}</div>`;break;}
1077 case 'dup':{const ps=sh.ids.map(pat);const f=state.form;const reason=p=>p.phone===f.phone.trim()?'Same mobile number':'Same name';head=`<span></span><span class="t"></span><span></span>`;
1078  body=`<div class="center" style="display:flex;flex-direction:column;align-items:center;gap:8px"><span class="tile" style="width:52px;height:52px;border-radius:50%;font-size:26px;background:var(--accent-subtle)">${I('userplus')}</span><div class="t1" style="font-size:18px">Possible existing patient${ps.length>1?'s':''}</div><div class="callout">${ps.length===1?'We found a patient with similar details.':`We found ${ps.length} patients with similar details.`}</div></div>
1079  <div class="group">${ps.map(p=>`<button class="row" onclick="state.form=null;state.sheet=null;stack().pop();A.push('patient',{id:'${p.id}'})">${avatar(p,44)}<span class="main"><div class="t1">${esc(p.name)}</div><div class="cap">MRN ${p.mrn} · ${p.age} yrs · ${esc(p.gender)}</div><div class="cap">${esc(p.phone)}</div><span class="tag" style="margin-top:4px">${reason(p)}</span></span>${I('right','chev')}</button>`).join('')}</div>
1080  <div class="cap" style="display:flex;gap:6px">${I('info')} Please confirm this is not the same patient, to avoid duplicate records.<
1080/div>`;
1081  foot=`<button class="btn p" onclick="state.form=null;state.sheet=null;stack().pop();A.push('patient',{id:'${ps[0].id}'})">View Patient</button><button class="btn s" onclick="A.savePatient(true)">Create Anyway</button>`;break;}
1082 case 'apptEditor':{const a=sh.id?DATA.appointments.find(x=>x.id===sh.id):null;const f=sh.f||(sh.f={pid:a?a.pid:(sh.pid||''),name:'',type:a?a.type:'followUp',at:a?a.at:dAdd(NOW,0,14,0),dur:a?a.dur:30,urgent:a?a.urgent:false,notes:a?a.notes:'',status:a?a.status:'Scheduled'});
1083  const local=d=>{const z=new Date(d.getTime()-d.getTimezoneOffset()*6e4);return z.toISOString().slice(0,16);};
1084  head=`<button class="quiet" onclick="A.closeSheet()">Cancel</button><span class="t">${a?'Edit Appointment':'New Appointment'}</span><span style="width:48px"></span>`;
1085  body=`<div class="fs"><div class="ov">Patient</div><div><label class="fl">Patient</label><div class="well"><select onchange="state.sheet.f.pid=this.value;render()"><option value="" ${f.pid?'':'selected'}>Not in records</option>${DATA.patients.map(p=>`<option value="${p.id}" ${f.pid===p.id?'selected':''}>${esc(p.name)}</option>`).join('')}</select>${I('down')}</div></div>${f.pid?'':`<div><label class="fl">Name <span class="req">*</span></label><div class="well"><input placeholder="Who the slot is for" value="${esc(f.name)}" oninput="state.sheet.f.name=this.value"></div></div>`}</div>
1086  <div class="fs"><div class="ov">Consultation</div><div><label class="fl">Type</label><div class="well"><select onchange="state.sheet.f.type=this.value;state.sheet.f.dur=VISIT[this.value].dur;render()">${Object.entries(VISIT).map(([k,v])=>`<option value="${k}" ${f.type===k?'selected':''}>${v.full}</option>`).join('')}</select>${I('down')}</div></div>
1087  <div class="two"><div><label class="fl">Starts</label><div class="well"><input type="datetime-local" value="${local(f.at)}" onchange="state.sheet.f.at=new Date(this.value)"></div></div><div><label class="fl">Duration</label><div class="well"><select onchange="state.sheet.f.dur=+this.value">${[15,20,30,45,50,60,90].map(m=>`<option value="${m}" ${f.dur===m?'selected':''}>${m} min</option>`).join('')}</select>${I('down')}</div></div></div>
1088  <div class="trow"><span>Urgent</span><button class="toggle ${f.urgent?'on':''}" onclick="state.sheet.f.urgent=!state.sheet.f.urgent;render()" aria-label="Urgent"></button></div></div>
1089  ${a?`<div class="fs"><div class="ov">Status</div><div class="seg">${['Scheduled','Completed','Cancelled'].map(s=>`<button class="${f.status===s?'on':''}" onclick="state.sheet.f.status='${s}';render()">${s}</button>`).join('')}</div></div>`:''}
1090  <div class="fs"><div class="ov">Notes</div><div><label class="fl">Reason for the appointment</label><div class="well ta"><textarea placeholder="Optional" oninput="state.sheet.f.notes=this.value">${esc(f.notes)}</textarea></div></div></div>`;
1091  foot=`<button class="btn p" onclick="A.saveAppt()">${a?'Save Changes':'Book Appointment'}</button>`;break;}
1092 case 'export':{const ev=DATA.evaluations.find(e=>e.id===sh.id);const p=pat(ev.pid);const secs=['Patient & Visit Information','Presenting Concern','Safety & Risk','Symptoms','History','Mental Status Examination','Scales','Formulation','Medication & Treatment','Plan & Follow-up','Additional Clinical Notes'];const t=sh.t||(sh.t={on:new Set(secs),mask:false,tr:false});
1093  head=`<button class="quiet" onclick="A.closeSheet()">Cancel</button><span class="t">Export Evaluation</span><span style="width:48px"></span>`;
1094  body=`<div class="cap center">${esc(p.name)}</div><div class="fs"><div class="ov">Include sections</div><div class="card" style="gap:0;padding:4px 16px">${secs.map(s=>`<div class="trow" style="border-top:1px solid var(--divider)"><span>${s}</span><button class="toggle ${t.on.has(s)?'on':''}" onclick="const o=state.sheet.t.on;o.has('${s}')?o.delete('${s}'):o.add('${s}');render()" aria-label="${s}"></button></div>`).join('').replace('border-top:1px solid var(--divider)','')}</div></div>
1095  <div class="fs"><div class="ov">Supporting information</div><div class="card" style="gap:0;padding:4px 16px"><div class="trow"><span><div>Hide patient identifying details</div><div class="cap">Masks patient personal information in this export.</div></span><button class="toggle ${t.mask?'on':''}" onclick="state.sheet.t.mask=!state.sheet.t.mask;render()" aria-label="Hide identifying details"></button></div>
1095<div class="trow" style="border-top:1px solid var(--divider)"><span><div>Full consultation transcript</div><div class="cap">${ev.transcript?'Recording available':'No recording'}</div></span><button class="toggle ${t.tr?'on':''}" onclick="state.sheet.t.tr=!state.sheet.t.tr;render()" aria-label="Include transcript"></button></div></div></div>`;
1096  foot=`<button class="btn p" onclick="A.closeSheet();A.toast('Structured PDF prepared · ${t.on.size} sections',2600)">${I('export')} Export Structured PDF</button>`;break;}
1097 case 'scaleResult':{let res;const s=SCALES[sh.sid];if(sh.result){res=DATA.evaluations.find(e=>e.id===sh.result).draft.assessmentResults.find(r=>r.instrument===sh.sid);}else res=D().assessmentResults.find(r=>r.instrument===sh.sid);
1098  head=`<span style="width:48px"></span><span class="t">${s.title}</span><button class="quiet" onclick="A.closeSheet()">Done</button>`;
1099  body=`<div><div class="t1" style="font-size:22px">${s.title} Result</div>${s.gate?`<div class="cap">Trauma exposure endorsed: ${res.trauma?'Yes':'No'}</div>`:''}<div class="cap">${severity(sh.sid,res.score)}</div></div><div class="card"><div class="lbl" style="font-weight:600">Score</div><div class="score">${res.score} / ${res.max}</div></div>${responsesList(sh.sid,res)}`;
1100  foot=sh.retake?`<button class="btn s" style="color:var(--danger)" onclick="A.sheet('confirm',{title:'Retake assessment?',msg:'The recorded ${s.title} result for this evaluation will be replaced when the new one is added.',ok:'Retake Assessment',on:'state.sheet=null;A.runScale(\\'${sh.sid}\\',\\'eval\\')'})">Retake Assessment</button>`:'';break;}
1101 case 'savedTranscript':{const ev=DATA.evaluations.find(e=>e.id===sh.id);head=`<button class="quiet" onclick="A.closeSheet()">${I('left')} Back</button><span class="t">Transcript</span><button class="quiet" onclick="A.copyText(DATA.evaluations.find(e=>e.id==='${ev.id}').transcript)">Copy</button>`;
1102  body=`<div><div class="t1" style="font-size:18px">Recording</div><div class="cap">${fmtDateUS(ev.date)} · ${Math.floor(ev.seconds/60)}:${String(ev.seconds%60).padStart(2,'0')}</div></div><div class="transcript">${ev.transcript?esc(ev.transcript):'No transcript was saved with this recording.'}</div>`;break;}
1103 }
1104 return `<div class="scrim" onclick="if(event.target===this)A.closeSheet()"><div class="sheet" role="dialog"><div class="grab"></div>${head?`<div class="sh">${head}</div>`:''}<div class="sb">${body}</div>${foot?`<div class="ft">${foot}</div>`:''}</div></div>`;}
1105A.saveAppt=()=>{const sh=state.sheet;const f=sh.f;if(!f.pid&&!f.name.trim()){A.toast('Enter who the slot is for');return;}
1106 if(sh.id){const a=DATA.appointments.find(x=>x.id===sh.id);Object.assign(a,{pid:f.pid||a.pid,name:f.pid?pat(f.pid).name:f.name,type:f.type,at:f.at,dur:f.dur,urgent:f.urgent,notes:f.notes,status:f.status});state.sheet=null;render();A.toast('Changes saved');return;}
1107 const p=f.pid?pat(f.pid):null;DATA.appointments.push({id:nid(),pid:f.pid||DATA.patients[0].id,name:p?p.name:f.name.trim(),at:f.at,type:f.type,dur:f.dur,notes:f.notes,status:'Scheduled',urgent:f.urgent,booked:new Date(NOW)});state.sheet=null;render();A.toast('Appointment booked');};
1108
1109/* ---------- render + boot ---------- */
1110const SCREENS={home:screenHome,patients:screenPatients,psearch:screenSearch,patient:screenPatient,newPatient:screenNewPatient,appts:screenAppts,appt:screenAppt,chooser:screenChooser,evalType:screenEvalType,entry:screenEntry,workspace:screenWorkspace,transcript:screenTranscript,review:screenReview,saved:screenSaved,tools:screenTools,scale:screenScale,settings:screenSettings};
1111let lastKey='',lastRailTab='';
1112function render(scrollTop){const t=topS();if(t.s!=='newPatient'&&state.form&&!state.sheet)state.form=null;
1113 const app=document.getElementById('app');const key=state.tab+'/'+stack().map(x=>x.s).join('/')+(state.session?'/'+state.session.tab:'');
1114 const scroller=app.querySelector('.content');const y=scroller&&!scrollTop&&key===lastKey?scroller.scrollTop:0;const focus=document.activeElement&&document.activeElement.id;
1115 const fs=FULLSCREEN.has(t.s)||(t.s==='chooser'&&state.session);
1116 app.innerHTML=`<div class="sbar"><span>9:41</span><span class="isl"></span><span class="r">●●●● <svg class="i" viewBox="0 0 24 24" style="font-size:14px"><path d="M2 9a14 14 0 0 1 20 0M5.5 12.5a9 9 0 0 1 13 0M9 16a4.5 4.5 0 0 1 6 0M12 19.5h.01"/></svg> <svg viewBox="0 0 26 12" style="width:24px;height:12px"><rect x="1" y="1" width="21" height="10" rx="3" fill="none" stroke="currentColor"/><rect x="3" y="3" width="17" height="6" rx="1.5" fill="currentColor"/><rect x="23" y="4" width="2" height="4" rx="1" fill="currentColor"/></svg></span></div>${SCREENS[t.s](t)}${fs?'':tabbar()}${sheetHTML()}${state.toast?`<div class="toast">${esc(state.toast)}</div>`:''}`;
1117 const sc=app.querySelector('.content');if(sc)sc.scrollTop=y;lastKey=key;
1118 if(focus){const el=document.getElementById(focus);if(el&&el.tagName==='INPUT'){el.focus();try{el.setSelectionRange(el.value.length,el.value.length);}catch(e){}}}
1119 const railTab=state.session?state.session.tab:'';const tb=app.querySelector('.tabs .tb.on');
1120 if(tb&&railTab!==lastRailTab)requestAnimationFrame(()=>tb.scrollIntoView({block:'nearest',inline:'center'}));
1121 lastRailTab=railTab;}
1122render();
1123/* Animate navigation with the View Transitions API where supported; elsewhere it stays instant. */
1124(function(){if(!document.startViewTransition||matchMedia('(prefers-reduced-motion: reduce)').matches)return;
1125 let busy=false;
1126 const wrap=(name,dir)=>{const fn=A[name];if(typeof fn!=='function')return;A[name]=function(...args){
1127  if(busy)return fn.apply(this,args);
1128  busy=true;const root=document.documentElement;
1128root.dataset.nav=typeof dir==='function'?dir(...args):dir;
1129  let t;try{t=document.startViewTransition(()=>fn.apply(this,args));}catch(e){busy=false;delete root.dataset.nav;return fn.apply(this,args);}
1130  t.finished.finally(()=>{busy=false;delete root.dataset.nav;});};};
1131 wrap('push','forward');wrap('pop','back');wrap('popTo','back');
1132 wrap('openPatient','forward');wrap('startEvalFor','forward');wrap('startSession','forward');
1133 wrap('tab','fade');wrap('wsNext','fade');wrap('wsBack','fade');})();
1134document.addEventListener('visibilitychange',()=>{if(document.hidden)A.stopTimer();else if(state.session&&state.session.rec.status==='recording')A.startTimer();},{passive:true});
1135/* splash: the app's launch screen, held for a few seconds, with an Interactive Demo badge */
1136/* held until its own animation has played (~1.7s); tap to skip; shown once per browser session */
1137(function(){let seen=false;try{seen=sessionStorage.getItem('dnDemoSplash')==='1';sessionStorage.setItem('dnDemoSplash','1');}catch(e){}
1138 if(seen)return;
1139 const sp=document.createElement('div');sp.className='splash';sp.style.setProperty('--tint',timeOfDayHue());
1140 sp.innerHTML=`<div class="ring"><svg class="o" viewBox="0 0 132 132" aria-hidden="true"><circle cx="66" cy="66" r="65"/></svg><img src="/assets/images/drnotes-demo-splash-logo.png" alt="" width="96" height="96" decoding="async"></div><div class="n1">Dr. Notes</div><div class="n2">for Psychiatrists</div><div class="n3">Interactive Demo</div>`;
1141 document.querySelector('.screen').appendChild(sp);
1142 let done=false;const hide=()=>{if(done)return;done=true;sp.classList.add('out');setTimeout(()=>sp.remove(),450);};
1143 sp.addEventListener('pointerdown',hide,{once:true});setTimeout(hide,1800);})();
1144
1145</script>
1145
1146
1147</body></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.