diff --git a/targets/carewell/CareWellTarget/Program.cs b/targets/carewell/CareWellTarget/Program.cs
index 4c2469c..3f62e41 100644
--- a/targets/carewell/CareWellTarget/Program.cs
+++ b/targets/carewell/CareWellTarget/Program.cs
@@ -214,7 +214,7 @@ app.MapPost("/api/appointments/commit", async (CommitRequest req) =>
             if (key == null)
             {
                 op = "INSERT";
-                key = Convert.ToString(await ScalarIn(conn, tx, db.AsText(db.NextVal("appointments_seq")) + db.FromDual));
+                key = Convert.ToString(await ScalarIn(conn, tx, "select " + db.AsText(db.NextVal("appointments_seq")) + db.FromDual));
                 ids.Add(key);
             }
             else { op = "UPDATE"; ids.Add(key); }
@@ -272,7 +272,7 @@ app.MapPost("/api/billing/commit", async (CommitRequest req) =>
         foreach (var r in req.Records ?? [])
         {
             index++;
-            var key = Convert.ToString(await ScalarIn(conn, tx, db.AsText(db.NextVal("payments_seq")) + db.FromDual));
+            var key = Convert.ToString(await ScalarIn(conn, tx, "select " + db.AsText(db.NextVal("payments_seq")) + db.FromDual));
             ids.Add(key);
             var values = payCols.Select(c => (object?)r.Values.GetValueOrDefault(c)).Append(key).ToArray();
             await using (var cmd = db.Command(conn,
diff --git a/targets/carewell/CareWellTarget/wwwroot/app.js b/targets/carewell/CareWellTarget/wwwroot/app.js
index 7413c47..ec312ff 100644
--- a/targets/carewell/CareWellTarget/wwwroot/app.js
+++ b/targets/carewell/CareWellTarget/wwwroot/app.js
@@ -156,12 +156,13 @@ const FORMS = {
       // select d.dept_id, d.first_name || ' ' || d.last_name into :appointments.dept_id, :appointments.doctor_name
       //   from doctors d where d.doctor_id = :appointments.doctor_id;
       // set_reason_required;
-      // (The assignments leave every queried appointment CHANGED, as in the original: a save updates them all.)
+      // (The assignments leave each queried appointment CHANGED once the client has fetched it, as in the
+      // original: a save updates every fetched record.)
       'POST-QUERY:APPOINTMENTS': (f, rec, row) => {
         rec.v.DEPT_ID = row.dept_id ?? '';
         rec.v.DOCTOR_NAME = row.doctor_name ?? '';
         setReasonRequired(rec);
-        rec.status = 'CHANGED';
+        rec.fetchChanged = true;
       },
       // a doctor of another department no longer fits: clear the doctor
       'WHEN-LIST-CHANGED:APPOINTMENTS.DEPT_ID': async f => {
@@ -523,6 +524,7 @@ function activateForm(f) {
   }
   renderAppbar();
   renderTrail();
+  updatePosition();
 }
 
 // ---- the library CW_LIB ----------------------------------------------------------------------------
@@ -792,7 +794,11 @@ async function navigate(f, to, opts = {}) {
       if (rec > from.rec) rec--;
     }
   }
-  if (rec >= tb.records.length) { tb.records.push(newRecord(f, to.block)); rec = tb.records.length - 1; }
+  if (rec >= tb.records.length) {
+    // entering a new record: the cursor goes to its first item, and the message line clears
+    tb.records.push(newRecord(f, to.block)); rec = tb.records.length - 1;
+    if (!tb.def.control) { to = { ...to, item: navItems(f, to.block)[0].name }; setMessage(''); }
+  }
   const masterMoved = tb.def.details && tb.cur !== rec;
   tb.cur = rec;
   scrollTo(f, to.block);
@@ -831,24 +837,29 @@ async function validateItem(f, ref) {
 }
 
 // Validate from list: the value must be the first column of a row of the item's LOV. A unique match by
-// prefix is taken; several open the list, reduced to them; none is refused.
+// prefix is taken; several open the list, reduced to them; none opens the whole list ("Choices in list: N"),
+// and closing it refuses the value (FRM-40212).
 async function validateFromList(f, ref, it, v) {
   const lov = f.def.lovs[it.lov];
   const rows = await lov.rows(f);
   const key = lov.columns[0][2];
   let row = rows.find(r => String(r[key] ?? '') === v);
   if (!row) {
+    if (!rows.length) { await msg('FRM-41830: List of Values contains no entries.'); return false; }
     const pre = rows.filter(r => String(r[key] ?? '').toUpperCase().startsWith(v.toUpperCase()));
-    if (!pre.length) { await msg('FRM-41830: List of Values contains no entries.'); return false; }
-    row = pre.length === 1 ? pre[0] : await showLov(lov, rows, v + '%');
-    focusCursor();
-    if (!row) return false;
+    if (pre.length === 1) row = pre[0];
+    else {
+      if (!pre.length) setMessage(`Choices in list: ${rows.length}`);
+      row = await showLov(lov, rows, pre.length ? v + '%' : '%');
+      focusCursor();
+      if (!row) { if (!pre.length) await msg(`FRM-40212: Invalid value for field ${it.name}.`); return false; }
+    }
   }
   assignReturns(f, lov, row, ref.rec);
   return true;
 }
 
-async function validateRecord(f, block, idx) {
+async function validateRecord(f, block, idx, commit) {
   const b = f.blocks[block], rec = b.records[idx];
   if (b.def.control || b.qbe || !rec || (rec.status !== 'INSERT' && rec.status !== 'CHANGED')) return true;
   for (const it of b.def.items) {
@@ -866,7 +877,12 @@ async function validateRecord(f, block, idx) {
   }
   const t = f.def.triggers['WHEN-VALIDATE-RECORD:' + block];
   if (t && !rec.validated) {
-    if (await t(f, rec, idx) === false) { focusCursor(); return false; }
+    if (await t(f, rec, idx) === false) {
+      // a record that fails validation while the form commits gets the cursor, on its first item
+      if (commit) { b.cur = idx; f.cursor = { block, item: navItems(f, block)[0].name, rec: idx }; scrollTo(f, block); renderBlock(f, block); }
+      focusCursor();
+      return false;
+    }
   }
   rec.validated = true;
   return true;
@@ -916,7 +932,10 @@ function masterHasRecord(f, master) {
 async function clearDetailsAsk(f, master) {
   for (const d of f.blocks[master].def.details) {
     if (blockStatus(f.blocks[d]) !== 'CHANGED') continue;
+    // Clear_All_Master_Details goes to the detail block: Forms is at block level, outside any record
+    S.blockLevel = true; updatePosition();
     const a = await askCommit();                  // Clear_Block(ASK_COMMIT)
+    S.blockLevel = false; updatePosition();
     if (a === 'Cancel') return false;
     if (a === 'Yes' && !await commitForm(f)) return false;
   }
@@ -934,6 +953,7 @@ async function populateDetails(f, master) {
       const rows = await b.def.fetch(f, b);
       b.records = rows.map(r => fromRow(f, b, r));
       queried(b, rows.length);
+      if (!rows.length) b.records.push(newRecord(f, d));    // a query without rows leaves a new record, defaults shown
     }
   }
   for (const d of md.details) {
@@ -973,13 +993,19 @@ async function executeQuery(f, block) {
   }
   let rows = await b.def.fetch(f, b);
   if (rows.error) { await msg(rows.error); return false; }
-  if (b.qbe) { rows = rows.filter(qbeFilter(f, b)); b.qbe = false; f.mode = 'NORMAL'; }
+  if (b.qbe) { rows = rows.filter(qbeFilter(f, b)); b.qbe = false; f.mode = 'NORMAL'; setMessage(''); }
   b.records = rows.map(r => fromRow(f, b, r));
   b.deleted = []; b.cur = 0; b.top = 0;
   queried(b, rows.length);
+  // a record POST-QUERY changed is validated as it is fetched; a refusal (WHEN-VALIDATE-RECORD) ends the fetch
+  const wvr = f.def.triggers['WHEN-VALIDATE-RECORD:' + block];
+  if (wvr) for (let i = 0; i < Math.min(b.records.length, b.def.display || 1); i++) {
+    if (b.records[i].fetchChanged && await wvr(f, b.records[i]) === false) {
+      b.records.length = i + 1; queried(b, i + 1); break;
+    }
+  }
   if (!b.records.length) { b.records = [newRecord(f, block)]; await msg('FRM-40350: Query caused no records to be retrieved.', 5); }
-  const item = f.cursor && f.cursor.block === block ? f.cursor.item : navItems(f, block)[0].name;
-  f.cursor = { block, item, rec: 0 };
+  f.cursor = { block, item: navItems(f, block)[0].name, rec: 0 };     // the first item of the first record
   renderBlock(f, block);
   await populateDetails(f, block);
   focusCursor();
@@ -1005,7 +1031,9 @@ async function enterQuery(f) {
   f.mode = 'ENTER-QUERY';
   f.cursor = { ...c, rec: 0 };
   renderBlock(f, c.block);
+  setMessage('Enter a query;  press Ctrl+F11 to execute, F4 to cancel.');
   focusCursor();
+  await fireWnri(f, c.block);
 }
 function qbeFilter(f, b) {
   const crit = Object.entries(b.records[0]?.v || {}).filter(([, v]) => v !== '');
@@ -1045,11 +1073,11 @@ async function deleteRecord(f) {
   const b = f.blocks[c.block], rec = b.records[b.cur];
   if (b.def.control || !b.def.delete) { await msg('FRM-41049: You cannot delete this record.'); return false; }
   if (!rec) return false;
-  if (rec.status === 'QUERY' || rec.status === 'CHANGED') b.deleted.push(rec.key);
+  if (rec.status === 'QUERY' || rec.status === 'CHANGED') { b.deleted.push(rec.key); if (b.queried !== undefined) b.deletedQ++; }
   b.records.splice(b.cur, 1);
   if (!b.records.length) b.records.push(newRecord(f, c.block));
   if (b.cur >= b.records.length) b.cur = b.records.length - 1;
-  f.cursor = { ...c, rec: b.cur };
+  f.cursor = { block: c.block, item: navItems(f, c.block)[0].name, rec: b.cur };   // the next record's first item
   scrollTo(f, c.block);
   renderBlock(f, c.block);
   if (b.def.details) await populateDetails(f, c.block);
@@ -1100,7 +1128,7 @@ async function commitForm(f) {
     changed: b.records.map((r, i) => [r, i]).filter(([r]) => r.status === 'INSERT' || r.status === 'CHANGED') }))
     .filter(w => w.changed.length || w.b.deleted.length);
   if (!work.length) { await msg('FRM-40401: No changes to save.', 5); return true; }
-  for (const w of work) for (const [, i] of w.changed) if (!await validateRecord(f, w.b.def.name, i)) return false;
+  for (const w of work) for (const [, i] of w.changed) if (!await validateRecord(f, w.b.def.name, i, true)) return false;
   let applied = 0;
   for (const { b, changed } of work) {
     const bd = b.def, keyItem = bd.key.toUpperCase();
@@ -1111,9 +1139,9 @@ async function commitForm(f) {
       // the keys PRE-INSERT drew stay in the records' items
       changed.forEach(([r], i) => { if (r.status === 'INSERT' && res.ids && res.ids[i]) r.v[keyItem] = oraNumber(res.ids[i]); });
       const at = res.index !== null && res.index !== undefined && res.index >= 0 ? changed[res.index] : null;
-      if (at) {
+      if (at) {      // the cursor goes to the record that failed, on its first item
         b.cur = at[1];
-        f.cursor = { block: bd.name, item: f.cursor && f.cursor.block === bd.name ? f.cursor.item : navItems(f, bd.name)[0].name, rec: at[1] };
+        f.cursor = { block: bd.name, item: navItems(f, bd.name)[0].name, rec: at[1] };
         scrollTo(f, bd.name);
       }
       renderBlock(f, bd.name);
@@ -1300,6 +1328,7 @@ function renderAppbar() {
   const bar = $('menubar'), tools = $('toolbar');
   bar.innerHTML = ''; tools.innerHTML = '';
   $('appbar').hidden = S.ended;
+  $('mdi-title').closest('.window').hidden = !S.mdiTitle;
   const withMenu = !!(f && f.def.menu);
   bar.hidden = tools.hidden = !withMenu;
   if (!withMenu) return;
@@ -1544,6 +1573,7 @@ async function scrollRows(f, block, delta) {
 function renderBlock(f, block) {
   if (!f.el) return;
   const b = f.blocks[block], bd = b.def;
+  fetchShown(b);
   const insertable = bd.insert && !(bd.master && !masterHasRecord(f, bd.master));
   for (const r of f.el.querySelectorAll(`.block[data-block="${block}"] .rec`)) {
     const ri = bd.control ? 0 : b.top + +r.dataset.row;
@@ -1572,7 +1602,6 @@ function renderBlock(f, block) {
     thumb.style.height = (100 * n / len) + '%';
     thumb.closest('.block').classList.toggle('scrollable', b.records.length > n);
   }
-  fetchShown(b);
 }
 
 function scrollTo(f, block) {
@@ -1604,21 +1633,25 @@ function focusCursor() {
 
 // A queried block fetches its rows in batches of the display count as they come into view; the record count
 // is known ('Record: n/m') only once a fetch came back short, until then it reads 'n/?'.
-function queried(b, n) { b.queried = n; b.batches = 1; }
+function queried(b, n) { b.queried = n; b.batches = 1; b.deletedQ = 0; }
 
 function fetchShown(b) {
   if (b.queried === undefined) return;
   const size = b.def.display || 1;
-  b.batches = Math.max(b.batches, Math.ceil((b.top + size) / size));
+  // another batch when the shown rows need more records than were fetched (scrolling, a deleted record)
+  while (Math.min(b.queried, b.batches * size) - b.deletedQ < b.top + size && b.batches * size < b.queried) b.batches++;
+  // POST-QUERY runs as a record is fetched: a record it changes counts as changed from then on
+  for (const r of b.records.slice(0, b.batches * size))
+    if (r.fetchChanged) { delete r.fetchChanged; if (r.status === 'QUERY') r.status = 'CHANGED'; }
 }
 
 function updatePosition() {
   const f = S.active;
-  if (!f || !f.cursor) { $('position').textContent = ''; return; }
+  if (!f || !f.cursor) { $('position').textContent = $('mode').textContent = ''; return; }
   const b = f.blocks[f.cursor.block];
   const known = b.queried === undefined || b.batches * (b.def.display || 1) > b.queried;
-  $('position').textContent = `Record: ${b.records.length ? b.cur + 1 : 0}/${known ? b.records.length : '?'}`
-    + (f.mode === 'ENTER-QUERY' ? '  Enter Query' : '');
+  $('position').textContent = `Record: ${b.records.length && !S.blockLevel ? b.cur + 1 : 0}/${known ? b.records.length : '?'}`;
+  $('mode').textContent = f.mode === 'ENTER-QUERY' ? 'Enter Query' : '';
 }
 
 // ---- events -------------------------------------------------------------------------------------------
diff --git a/targets/carewell/CareWellTarget/wwwroot/index.html b/targets/carewell/CareWellTarget/wwwroot/index.html
index eb38a1b..900dba6 100644
--- a/targets/carewell/CareWellTarget/wwwroot/index.html
+++ b/targets/carewell/CareWellTarget/wwwroot/index.html
@@ -14,7 +14,7 @@
   <aside class="demo-notice"><span class="wide">Migrated from Oracle Forms to .NET and PostgreSQL, behaviour proven identical to the original.</span>
     Shared demo, data resets nightly. Sign in as any listed user: <b>BILLING1</b> sees the billing modules, <b>RECEPTION1</b> the front desk.</aside>
   <div id="appbar" hidden>
-    <div id="mdi-title"></div>
+    <div class="window frame" data-window="FRAME"><div class="titlebar"><span id="mdi-title"></span></div></div>
     <nav id="menubar" aria-label="Menu"></nav>
     <div id="toolbar" role="toolbar" aria-label="Toolbar"></div>
   </div>
@@ -23,7 +23,7 @@
     <div id="mdi"></div>
     <div id="ended" hidden><div class="ended-card"><h2>The application has ended.</h2><p>Exiting the last form ends the Forms session, as in the original.</p><a class="btn primary" href="">Start again</a></div></div>
   </main>
-  <footer id="statusbar"><span id="message" role="status"></span><span id="position"></span></footer>
+  <footer id="statusbar"><span id="message" role="status"></span><span id="mode"></span><span id="position"></span></footer>
 </div>
 <div id="alert" class="modal" role="alertdialog" aria-modal="true" hidden>
   <div class="dialog"><div class="dlg-title" id="alert-title"></div>
diff --git a/targets/carewell/CareWellTarget/wwwroot/style.css b/targets/carewell/CareWellTarget/wwwroot/style.css
index 33dcc6e..a54cfc0 100644
--- a/targets/carewell/CareWellTarget/wwwroot/style.css
+++ b/targets/carewell/CareWellTarget/wwwroot/style.css
@@ -25,12 +25,16 @@ button, input, select { font: inherit; color: inherit; }
 #statusbar.has-message #message::before { content: "\2139"; display: inline-block; margin-right: 8px; color: var(--accent); font-weight: 700; }
 #statusbar.flash { animation: flash 1.2s ease-out; }
 @keyframes flash { from { background: #fff1c2; } to { background: #fff; } }
+#mode { color: var(--accent); font-weight: 600; white-space: nowrap; }
+#mode:empty { display: none; }
 #position { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
 
 /* The MDI frame: its title (user and role), the menu CW_APP and the toolbar */
 #appbar { display: flex; align-items: center; gap: 4px 16px; flex-wrap: wrap; padding: 4px 24px; background: #fff; border-bottom: 1px solid var(--line); }
 #appbar[hidden] { display: none; }
-#mdi-title { font-size: 13px; font-weight: 600; color: var(--muted); order: 3; margin-left: auto; }
+#appbar .window.frame { order: 3; margin-left: auto; width: auto; border: none; border-radius: 0; box-shadow: none; background: none; cursor: default; }
+#appbar .frame .titlebar { border: none; background: none; padding: 0; min-height: 0; letter-spacing: 0; }
+#mdi-title { font-size: 13px; font-weight: 600; color: var(--muted); }
 #menubar { display: flex; gap: 2px; }
 #menubar[hidden], #toolbar[hidden] { display: none; }
 .menu { position: relative; }
@@ -207,7 +211,7 @@ a.btn { display: inline-flex; align-items: center; text-decoration: none; }
   .demo-notice { padding: 6px 14px; font-size: 12px; }
   .demo-notice .wide { display: none; }
   #appbar { padding: 4px 12px; }
-  #mdi-title { width: 100%; margin-left: 0; order: 0; }
+  #appbar .window.frame { width: 100%; margin-left: 0; order: 0; }
   #main { padding: 12px; }
   #statusbar { padding: 6px 12px; }
   .body { padding: 14px; }
diff --git a/targets/carewell/schema.sql b/targets/carewell/schema.sql
index 02da827..3e28b42 100644
--- a/targets/carewell/schema.sql
+++ b/targets/carewell/schema.sql
@@ -68,7 +68,7 @@ create table rooms (
 );
 
 create table appointments (
-  appt_id       numeric(8)    constraint appointments_pk primary key,
+  appt_id       numeric(8)    default nextval('appointments_seq') constraint appointments_pk primary key,  -- PRE-INSERT
   patient_id    numeric(8)    not null constraint appointments_patient_fk references patients,
   doctor_id     numeric(6)    not null constraint appointments_doctor_fk references doctors,
   appt_start    timestamp(0)  not null,
@@ -140,7 +140,7 @@ create table invoice_lines (
 );
 
 create table payments (
-  payment_id  numeric(8)     constraint payments_pk primary key,
+  payment_id  numeric(8)     default nextval('payments_seq') constraint payments_pk primary key,  -- PRE-INSERT
   invoice_id  numeric(8)     not null constraint payments_invoice_fk references invoices,
   paid_on     timestamp(0)   not null,
   amount      numeric(10,2)  not null constraint payments_amount_ck check (amount > 0),
