sign-off-axi 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,9 +88,9 @@ One line on what the agent runs per kind; the full reference is [docs/question-k
88
88
  | Proof update | `post --type evidence --media shot.png` | the capture, its result, Pass / Fail / Needs work |
89
89
  | Proof with a file to install | `post --type evidence --media shot.png --attach app.apk` | the capture, and the file with its size and a Download button (any type, up to 1 GB, `SIGN_OFF_ATTACH_MAX_MB` to change) |
90
90
  | Explainer video | `post --type explainer --media clip.mp4 --chapter 0:00=Start` | a player with chapter buttons, then Yes or No |
91
- | Report page | `report init <folder>`, then `post --type report --view ...` | a page to tap through |
91
+ | Report | `report init <folder>`, fill `report.json`, then `post --kind report --from <folder>/report.json` | one scroll: the answer and a picture, what I understood, what I found, the next step; numbers behind Details, Go deeper on every part, who read it and when |
92
92
  | Diagram, interactive page | `post --type diagram --view flow.svg` | the picture on the card; interactive pages run sandboxed |
93
- | Plan | `post --kind plan --from plan.json` | sections to approve or change, decisions to answer |
93
+ | Plan | `report init <folder> --plan`, fill `plan.json`, then `post --kind plan --from plan.json` | the same scroll, plus each decision with my pick and a Go ahead |
94
94
  | Sign-in link | `post --type signin --service npm --link <url> --unlocks ...` | an Open button and the time left |
95
95
 
96
96
  ## How agents use it
package/dist/cli.js CHANGED
@@ -40,7 +40,10 @@ examples:
40
40
  sign-off-axi post .sign-off/decisions.json --type design --title "Pick the sign-in layout" --option a=A --option b=B --mock a=mocks/a.html --mock b=mocks/b.html --recommend b
41
41
  sign-off-axi post .sign-off/decisions.json --type diagram --title "Is this the sign-in flow?" --view mocks/sign-in/flow.svg
42
42
  sign-off-axi post --type explainer --title "Is the new sign-in clear?" --about "The new sign-in has 2 steps." --media explainer.mp4 --chapter 0:00=Start
43
- sign-off-axi report init .sign-off/mocks/sign-in-flow
43
+ sign-off-axi report init .sign-off/reports/sign-in-flow
44
+ sign-off-axi post --kind report --from .sign-off/reports/sign-in-flow/report.json
45
+ sign-off-axi report init .sign-off/plans/sign-in --plan
46
+ sign-off-axi post --kind plan --from .sign-off/plans/sign-in/plan.json --filer my-session-id
44
47
  sign-off-axi deeper
45
48
  sign-off-axi post .sign-off/decisions.json --type report --title "Sign-in now takes 2 steps" --about "Why the code step moved first." --view mocks/sign-in-flow/index.html --preview mocks/sign-in-flow/preview.svg
46
49
  sign-off-axi post --type signin --key npm-login --service npm --link https://example.com/login --unlocks "Publish the release" --waits "Main" --expires 15m
@@ -94,7 +97,7 @@ examples:
94
97
  validate: `usage: sign-off-axi validate <message.json>... [--kind <kind>] [--expect-invalid]
95
98
  Checks message files against the communication contract without posting them: ${CONTRACT}
96
99
  A message is one JSON object with "contract": 1, its "kind" and its "schema". This build checks kinds ask, evidence, gate, finding and design at schema 1, report at schema 3 (schema 2 still passes) and plan at schema 2 (schema 1 still passes).
97
- A report 3 and a plan 2 are guided pages: fields the board draws as one scroll, no page of your own.
100
+ A report 3 and a plan 2 are guided pages: fields the board draws as one scroll, no page of your own (see report init).
98
101
  An evidence message's media paths read from the message file's folder: each file must be there, inside it, and 50 MB or less.
99
102
  A report is a page to tap through: its "view" (.html) and "preview" (its picture, beside it) are checked for their shape here; the files themselves are checked at post, from the board file's folder, filled with no FILL: lines left (see report init).
100
103
  A plan's "view" (.html) is checked for its shape here; the file itself is checked at post, from the board file's folder.
@@ -277,7 +280,13 @@ examples:
277
280
  restore: `usage: sign-off-axi restore <key> [board.json]
278
281
  Brings back a card that dismiss hid. See \`sign-off-axi dismiss --help\`.
279
282
  `,
280
- report: `usage: sign-off-axi report init <folder>
283
+ report: `usage: sign-off-axi report init <folder> [--plan] [--page]
284
+ Writes the guided template to fill: <folder>/report.json, or <folder>/plan.json with --plan. A file already there is kept.
285
+ The board draws it as one scroll: what this is (the answer and one picture), what I understood, what I found and recommend, what you decide (plans), and the next step.
286
+ Numbers sit behind a Details tap; every part has a Go deeper button that asks you (see \`sign-off-axi deeper --help\`) and a read receipt that shows when the person read it.
287
+ Replace every FILL: line, then post it: post --kind report --from <folder>/report.json, or post --kind plan --from <folder>/plan.json --filer <your id>.
288
+ A report is for an explanation the user asked for, or a concept, design or trade-off they must understand. Proof that something works is evidence.
289
+ --page writes the older scaffold page instead, for a report that needs a page of its own (report schema 2). It is:
281
290
  Writes a report page to fill: <folder>/index.html and its card picture <folder>/preview.svg. A file already there is kept.
282
291
  The page is one self-contained file, phone first, light and dark, in four parts, each one idea:
283
292
  1. the answer in one plain sentence and a picture of it 2. a diagram of how it works
@@ -1353,7 +1362,7 @@ function postReport(file, named, on, values) {
1353
1362
  throw fail(`--${odd} does not apply to --type report: a report takes no answer`, 'VALIDATION_ERROR', 'Run `sign-off-axi post --help`');
1354
1363
  const missing = ['title', 'about', 'view', 'preview'].filter((f) => values[f] === undefined).map((f) => `--${f}`);
1355
1364
  if (missing.length) {
1356
- throw fail(`a report is a page to tap through, not text: ${missing.join(' and ').replace(/ and (?=.* and )/g, ', ')} ${missing.length > 1 ? 'are' : 'is'} missing`, 'VALIDATION_ERROR', `Run \`${REPORT_INIT}\` to make the page and its picture, fill them, then post --type report --title "<the answer>" --about "<why it matters>" --view <folder>/index.html --preview <folder>/preview.svg`);
1365
+ throw fail(`a report is a page to tap through, not text: ${missing.join(' and ').replace(/ and (?=.* and )/g, ', ')} ${missing.length > 1 ? 'are' : 'is'} missing`, 'VALIDATION_ERROR', `Run \`${REPORT_INIT} --page\` to make the page and its picture, fill them, then post --type report --title "<the answer>" --about "<why it matters>" --view <folder>/index.html --preview <folder>/preview.svg`);
1357
1366
  }
1358
1367
  const key = values.key ?? slug(values.title), had = existsSync(file) ? readItems(file).items.find((i) => i?.key === key) : undefined;
1359
1368
  // Posting the same report again is a no-op, so it keeps the time it was first filed.
@@ -2002,21 +2011,38 @@ function init(args) {
2002
2011
  `Run \`sign-off-axi doctor ${shellPath(dir)}\` to check the setup`, `Run \`sign-off-axi open ${shellPath(board)}\` to show the board to the user`
2003
2012
  ]);
2004
2013
  }
2005
- /** report init <folder>: the scaffold page and its picture, kept when already there, with the post command that names them. */
2014
+ /** report init <folder>: the guided template (report.json, or plan.json with --plan) to fill in, or with --page the older scaffold page and its picture. */
2006
2015
  function report(args) {
2007
2016
  let parsed;
2008
2017
  try {
2009
- parsed = parseArgs({ args, allowPositionals: true, strict: true, options: {} });
2018
+ parsed = parseArgs({ args, allowPositionals: true, strict: true, options: { plan: { type: 'boolean' }, page: { type: 'boolean' } } });
2010
2019
  }
2011
2020
  catch (e) {
2012
2021
  throw fail(e.message, 'VALIDATION_ERROR', 'Run `sign-off-axi report --help`');
2013
2022
  }
2014
- const [sub, dir, extra] = parsed.positionals;
2015
- if (sub !== 'init' || dir === undefined || extra !== undefined) {
2016
- throw fail(sub !== 'init' ? `unknown report command ${sub ?? '(none)'}` : extra !== undefined ? `unexpected argument ${extra}: report init takes one folder` : 'report init needs the folder to write the page in', 'VALIDATION_ERROR', 'sign-off-axi report init .sign-off/mocks/<key>');
2023
+ const [sub, dir, extra] = parsed.positionals, { plan, page } = parsed.values;
2024
+ if (sub !== 'init' || dir === undefined || extra !== undefined || (plan && page)) {
2025
+ throw fail(sub !== 'init' ? `unknown report command ${sub ?? '(none)'}` : extra !== undefined ? `unexpected argument ${extra}: report init takes one folder` : plan && page ? '--plan and --page do not go together' : 'report init needs the folder to write the template in', 'VALIDATION_ERROR', 'sign-off-axi report init <folder> [--plan]');
2017
2026
  }
2018
2027
  const out = resolve(dir), created = [], kept = [];
2019
2028
  mkdirSync(out, { recursive: true });
2029
+ // The board serves a page from its own folder, so the post command names it from there.
2030
+ const board = boardHere(), rel = relative(dirname(board), out).split(sep).join('/'), served = rel && !rel.startsWith('../') && rel !== '..' && !isAbsolute(rel);
2031
+ if (!page) {
2032
+ const kind = plan ? 'plan' : 'report', path = join(out, `${kind}.json`);
2033
+ if (existsSync(path))
2034
+ kept.push(shellPath(path));
2035
+ else {
2036
+ const filed = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
2037
+ writeFileSync(path, readFileSync(new URL(`../web/${kind}-template.json`, import.meta.url), 'utf8').replace('"FILL: UTC time"', JSON.stringify(filed)), { flag: 'wx' });
2038
+ created.push(shellPath(path));
2039
+ }
2040
+ return toon({ folder: shellPath(out), created: created.length ? created : 'nothing new (re-run is a no-op)', kept }, [
2041
+ `Replace every FILL: line in ${kind}.json with your own words: the answer first, one picture (bars, steps or tiles), what you understood, then the sections${plan ? ' and the decisions' : ''}`,
2042
+ 'Keep each line short: the board shows it in one scroll, the numbers behind a Details tap and the long answer behind Go deeper',
2043
+ `Run \`sign-off-axi post --kind ${kind} --from ${shellPath(path)}\` to put it on the board`,
2044
+ ]);
2045
+ }
2020
2046
  for (const [name, from] of [['index.html', 'report.html'], ['preview.svg', 'report-preview.svg']]) {
2021
2047
  const path = join(out, name);
2022
2048
  if (existsSync(path))
@@ -2026,8 +2052,6 @@ function report(args) {
2026
2052
  created.push(shellPath(path));
2027
2053
  }
2028
2054
  }
2029
- // The board serves a report's page from its own folder, so the post command names the page from there.
2030
- const board = boardHere(), rel = relative(dirname(board), out).split(sep).join('/'), served = rel && !rel.startsWith('../') && rel !== '..' && !isAbsolute(rel);
2031
2055
  return toon({ folder: shellPath(out), created: created.length ? created : 'nothing new (re-run is a no-op)', kept }, [
2032
2056
  'Replace every FILL: line: the answer and its picture first, then the diagram, the steps to tap, and the folded detail',
2033
2057
  'Open index.html in a browser 390 px wide and look before posting: mostly text means it is not ready',
package/dist/contract.js CHANGED
@@ -42,7 +42,8 @@ const ACTIONS = {
42
42
  export const MEDIA_BYTES = 50_000_000;
43
43
  /** The command that makes a report's page, named in every refusal of a report without one. */
44
44
  export const REPORT_INIT = 'sign-off-axi report init <folder>';
45
- const PAGE_HELP = `Run \`${REPORT_INIT}\` to make the page, fill it, then post it with "view" and "preview"`;
45
+ const PAGE_HELP = `Run \`${REPORT_INIT} --page\` to make the page, fill it, then post it with "view" and "preview"`;
46
+ const GUIDE_HELP = `Run \`${REPORT_INIT}\` to write report.json, fill every FILL: line, then post it with \`post --kind report --from <folder>/report.json\``;
46
47
  /** Parses one message; a field written twice in any object refuses it, since plain JSON.parse keeps the last one quietly. */
47
48
  export function parseMessage(bytes) {
48
49
  let text, doc;
@@ -296,7 +297,7 @@ export function checkMessage(m, want, flags = false) {
296
297
  if (!whole(m.schema))
297
298
  throw bad('schema', `must be a whole number, not ${show(m.schema)}`, `Set "schema": ${SCHEMAS[m.kind]}`);
298
299
  if (m.kind === 'report' && m.schema === 1)
299
- throw bad('schema', '1 of report is text only: a report must be a page to tap through, with "view" and "preview"', PAGE_HELP);
300
+ throw bad('schema', '1 of report is text only: a report must be a page to tap through, with "view" and "preview"', GUIDE_HELP);
300
301
  if (m.schema !== SCHEMAS[m.kind] && !OLDER[m.kind]?.includes(m.schema))
301
302
  throw bad('schema', `${m.schema} of ${m.kind} is not in this sign-off-axi`, `Upgrade sign-off-axi, or post ${m.kind} schema ${SCHEMAS[m.kind]}`);
302
303
  if (g && /FILL:/.test(JSON.stringify(m)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sign-off-axi",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "One review board for evidence, quick yes/no decisions and retro changes.",
5
5
  "type": "module",
6
6
  "bin": { "sign-off-axi": "dist/cli.js" },
package/web/index.html CHANGED
@@ -760,6 +760,182 @@
760
760
  .ev-title { font-size: 2.5rem; line-height: 1.1; margin-top: 1.25rem; }
761
761
  .ev-intro { font-size: 1.3125rem; }
762
762
  }
763
+ /* The guided plan and report: one scroll with a rail and Your answers on a desk, a progress bar and a Go ahead bar on a phone. */
764
+ #reader .gd-scroll { flex: 1; min-height: 0; overflow: auto; padding: 0 1rem 7rem; scroll-behavior: smooth; }
765
+ #reader .gd, #reader .gd-bar { --hair: color-mix(in srgb, var(--text) 12%, transparent); --hair2: color-mix(in srgb, var(--text) 7%, transparent); --tint: color-mix(in srgb, var(--action) 11%, transparent);
766
+ --ok-bg: color-mix(in srgb, var(--ok) 13%, transparent); --warn-bg: color-mix(in srgb, var(--warn) 13%, transparent); --bad: var(--danger); --bad-bg: color-mix(in srgb, var(--danger) 11%, transparent); --well: var(--page); }
767
+ @media (prefers-color-scheme: dark) { #reader .gd, #reader .gd-bar { --well: #0C0C0D; } }
768
+ #reader .gd { max-width: 72rem; margin: 0 auto; font-size: 1.0625rem; line-height: 1.47; }
769
+ :where(#reader .gd, #reader .gd-bar) :where(button, input, textarea) { font: inherit; color: inherit; letter-spacing: inherit; }
770
+ :where(#reader .gd, #reader .gd-bar) button { cursor: pointer; border: 0; background: none; padding: 0; }
771
+ #reader .gd [hidden] { display: none !important; }
772
+ #reader .gd .ic { width: 1.25rem; height: 1.25rem; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
773
+ #reader .gd :focus-visible { outline: 2px solid var(--action); outline-offset: 2px; border-radius: 0.5rem; }
774
+ .gd-rail { display: none; }
775
+ .gd-progress { position: sticky; top: 0; z-index: 20; margin: 0 -1rem; padding: 0.625rem 1rem 0.5rem; background: color-mix(in srgb, var(--page) 94%, transparent); -webkit-backdrop-filter: saturate(180%) blur(16px); backdrop-filter: saturate(180%) blur(16px); border-bottom: 1px solid var(--hair2); }
776
+ .gd-progress .now { display: flex; justify-content: space-between; font-size: 0.875rem; color: var(--muted); margin-bottom: 0.5rem; }
777
+ .gd-progress .now b { color: var(--text); font-weight: 600; }
778
+ .gd-segs { display: flex; gap: 0.25rem; }
779
+ .gd-segs button { flex: 1; min-height: 0; height: 0.375rem; border-radius: 0.1875rem; background: var(--sunk); position: relative; }
780
+ .gd-segs button::after { content: ""; position: absolute; inset: -0.875rem 0; }
781
+ .gd-segs button.on { background: color-mix(in srgb, var(--action) 40%, var(--sunk)); }
782
+ .gd-segs button.cur { background: var(--action); height: 0.5rem; margin-top: -0.0625rem; }
783
+ .gd-main { min-width: 0; max-width: 45rem; margin: 0 auto; }
784
+ @media (min-width: 62.5rem) {
785
+ #reader .gd-scroll { padding: 0 2rem 4rem; }
786
+ .gd-cols { display: grid; grid-template-columns: 17rem minmax(0, 45rem); gap: 3rem; justify-content: center; align-items: start; margin-top: 0.5rem; }
787
+ .gd-rail { display: block; position: sticky; top: 1rem; }
788
+ .gd-progress, .gd-bar { display: none !important; }
789
+ #reader .gd-scroll { padding-bottom: 4rem; }
790
+ }
791
+ .gd-rail h4 { margin: 0 0 0.5rem; font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
792
+ .gd-rail nav { display: flex; flex-direction: column; gap: 0.125rem; }
793
+ .gd-rail nav a { display: flex; gap: 0.625rem; align-items: flex-start; padding: 0.5rem 0.625rem; border-radius: 0.625rem; color: var(--text); text-decoration: none; font-size: 0.9375rem; line-height: 1.35; }
794
+ .gd-rail nav a:hover { background: var(--sunk); }
795
+ .gd-rail nav a.cur { background: var(--tint); color: var(--accent); font-weight: 600; }
796
+ .gd-rail nav a .gn { width: 1.375rem; height: 1.375rem; border-radius: 50%; border: 1.5px solid var(--hair); display: grid; place-items: center; font-size: 0.75rem; font-weight: 600; color: var(--muted); flex: none; margin-top: 1px; }
797
+ .gd-rail nav a.seen .gn { background: var(--ok); border-color: var(--ok); color: var(--on-accent); }
798
+ .gd-rail nav a.seen .gn .ic { width: 0.8125rem; height: 0.8125rem; stroke-width: 2.6; }
799
+ .gd-sum { margin-top: 1.25rem; padding: 1rem; border-radius: 1rem; background: var(--tile); border: 1px solid var(--hair2); }
800
+ .gd-sum h4 { margin-bottom: 0.375rem; }
801
+ .gd-sum ul { list-style: none; margin: 0 0 0.875rem; padding: 0; font-size: 0.9375rem; line-height: 1.35; }
802
+ .gd-sum li { display: flex; justify-content: space-between; gap: 0.625rem; padding: 0.25rem 0; }
803
+ .gd-sum li span:first-child { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
804
+ .gd-sum li span:last-child { color: var(--muted); text-align: right; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
805
+ .gd-sum li .chg { color: var(--accent); font-weight: 600; }
806
+ .gd-top { padding: 0.75rem 0 1.125rem; }
807
+ .gd-top .geye { margin: 0 0 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--accent); }
808
+ #reader .gd-top h1 { margin: 0 0 0.625rem; font-size: 2.125rem; line-height: 1.1; letter-spacing: -0.034em; font-weight: 700; white-space: normal; overflow: visible; }
809
+ .gd-top .sub { margin: 0; color: var(--muted); font-size: 1.1875rem; line-height: 1.42; }
810
+ .gd-top .howto { margin-top: 1rem; padding: 0.75rem 0.875rem; border-radius: 0.875rem; background: var(--tint); font-size: 0.9375rem; line-height: 1.4; display: flex; gap: 0.625rem; align-items: flex-start; }
811
+ .gd-top .howto .ic { margin-top: 0.125rem; color: var(--accent); }
812
+ @media (min-width: 43.75rem) { #reader .gd-top h1 { font-size: 2.75rem; letter-spacing: -0.04em; } .gd-top .sub { font-size: 1.3125rem; } }
813
+ .gd-stage { display: flex; align-items: center; gap: 0.625rem; margin: 1.75rem 0 0.625rem; font-size: 0.875rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); scroll-margin-top: 5.5rem; }
814
+ .gd-stage i { font-style: normal; width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--sunk); display: grid; place-items: center; font-size: 0.8125rem; color: var(--text); letter-spacing: 0; }
815
+ .gd-stage::after { content: ""; flex: 1; height: 1px; background: var(--hair2); }
816
+ .gd-sec { background: var(--tile); border: 1px solid var(--hair2); border-radius: 1.375rem; padding: 1.25rem 1.125rem 0.875rem; margin: 0 0 0.875rem; scroll-margin-top: 5.5rem; }
817
+ @media (min-width: 43.75rem) { .gd-sec { padding: 1.75rem 2rem 1.125rem; border-radius: 1.5rem; } }
818
+ .gd-sec > .gh .gk { display: block; font-size: 0.875rem; font-weight: 600; color: var(--muted); margin-bottom: 0.375rem; }
819
+ #reader .gd-sec h2 { margin: 0 0 0.5rem; font-size: 1.5rem; line-height: 1.18; letter-spacing: -0.028em; font-weight: 700; }
820
+ @media (min-width: 43.75rem) { #reader .gd-sec h2 { font-size: 1.75rem; } }
821
+ .gd-sec > .gh:not(:has(.gd-lead)) h2 { margin-bottom: 0.875rem; }
822
+ .gd-lead { margin: 0 0 1rem; line-height: 1.5; }
823
+ .gd-fig { margin: 0.25rem 0 1rem; border-radius: 1rem; background: var(--well); padding: 1rem; }
824
+ .gd-cap { margin: 0.625rem 0 0; font-size: 0.875rem; color: var(--muted); line-height: 1.4; }
825
+ .gd-acts { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.125rem 0 0.25rem; }
826
+ .gd-lnk { display: inline-flex; align-items: center; gap: 0.375rem; min-height: 2.75rem; padding: 0 0.875rem; border-radius: 999px; background: var(--sunk); font-size: 0.9375rem; font-weight: 600; color: var(--text); }
827
+ .gd-lnk.deep { color: var(--accent); background: var(--tint); }
828
+ .gd-lnk .ic { width: 1.0625rem; height: 1.0625rem; transition: transform 0.2s; }
829
+ .gd-lnk[aria-expanded=true] .ic.gchev { transform: rotate(180deg); }
830
+ .gd-panel { margin: 0.625rem 0 0.375rem; border-radius: 1rem; background: var(--well); padding: 0.875rem 1rem; font-size: 1rem; line-height: 1.5; }
831
+ .gd-panel[hidden] { display: none; }
832
+ .gd-dt { width: 100%; border-collapse: collapse; font-size: 0.9375rem; line-height: 1.35; }
833
+ .gd-dt th, .gd-dt td { padding: 0.5625rem 0.375rem; text-align: left; border-bottom: 1px solid var(--hair2); vertical-align: top; }
834
+ .gd-dt th { font-weight: 500; color: var(--muted); }
835
+ .gd-dt td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
836
+ .gd-dt tr:last-child td, .gd-dt tr:last-child th { border-bottom: 0; }
837
+ .gd-ans { border: 1px solid var(--hair2); }
838
+ .gd-ans .gask { padding: 0 0 0.75rem; margin: 0 0 0.75rem; border-bottom: 1px solid var(--hair2); }
839
+ .gd-ans .gask:last-of-type { border-bottom: 0; margin-bottom: 0.25rem; padding-bottom: 0; }
840
+ .gd-ans .hd { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 600; color: var(--muted); margin-bottom: 0.5rem; }
841
+ .gd-ans .dot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--faint); flex: none; }
842
+ .gd-ans .live .dot { background: var(--action); animation: gd-pulse 1s ease-in-out infinite; }
843
+ @keyframes gd-pulse { 50% { opacity: 0.25; } }
844
+ .gd-ans .q { font-weight: 600; margin: 0 0 0.5rem; }
845
+ .gd-ans p { margin: 0 0 0.625rem; }
846
+ .gd-ans .caret { display: inline-block; width: 2px; height: 1.05em; background: var(--action); vertical-align: -0.15em; margin-left: 1px; animation: gd-pulse 1s steps(2) infinite; }
847
+ .gd-ans .shim { height: 0.625rem; border-radius: 0.3125rem; background: linear-gradient(90deg, var(--sunk), var(--hair), var(--sunk)); background-size: 200% 100%; animation: gd-sh 1.2s linear infinite; margin: 0.5rem 0; }
848
+ @keyframes gd-sh { to { background-position: -200% 0; } }
849
+ .gd-ans .slow { font-size: 0.875rem; color: var(--muted); margin: 0.25rem 0 0; }
850
+ .gd-fu { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
851
+ .gd-fu input { flex: 1; min-width: 0; min-height: 2.75rem; border-radius: 0.75rem; border: 1px solid var(--hair); background: var(--tile); padding: 0 0.75rem; }
852
+ .gd-fu button { min-height: 2.75rem; padding: 0 1rem; border-radius: 0.75rem; background: var(--action); color: var(--on-accent); font-weight: 600; }
853
+ .gd-chip { display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.8125rem; line-height: 1.25; padding: 0.375rem 0.625rem; border-radius: 0.75rem; background: var(--sunk); color: var(--muted); max-width: 100%; text-align: left; }
854
+ .gd-chip.ok { background: var(--ok-bg); color: var(--ok); } .gd-chip.bad { background: var(--bad-bg); color: var(--bad); } .gd-chip.warn { background: var(--warn-bg); color: var(--warn); } .gd-chip.acc { background: var(--tint); color: var(--accent); }
855
+ .gd-rc { display: flex; flex-wrap: wrap; gap: 0.125rem 1rem; margin: 0.625rem -0.125rem 0; padding: 0.625rem 0.125rem 0.125rem; border-top: 1px solid var(--hair2); font-size: 0.875rem; line-height: 1.35; color: var(--muted); }
856
+ .gd-rc span { display: inline-flex; align-items: center; gap: 0.375rem; }
857
+ .gd-rc .gdot { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--ok); flex: none; }
858
+ .gd-rc .gdot.hollow { background: none; border: 1.5px solid var(--faint); width: 0.5625rem; height: 0.5625rem; }
859
+ .gd-rc .gdot.agent { background: var(--action); }
860
+ .gd-rc b { font-weight: 600; color: var(--text); }
861
+ .gd-rc.s { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 0; }
862
+ .gd-uq { display: grid; margin: 0 0 1rem; border-radius: 1rem; overflow: hidden; border: 1px solid var(--hair2); }
863
+ .gd-uq div { padding: 0.75rem 0.875rem; border-bottom: 1px solid var(--hair2); }
864
+ .gd-uq div:last-child { border-bottom: 0; }
865
+ .gd-uq dt { font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); margin: 0 0 0.125rem; }
866
+ .gd-uq dd { margin: 0; }
867
+ .gd-seg { display: flex; gap: 0.5rem; flex-wrap: wrap; }
868
+ .gd-choice { min-height: 2.75rem; padding: 0 1.125rem; border-radius: 999px; border: 1.5px solid var(--hair) !important; background: var(--tile) !important; font-weight: 600; display: inline-flex; align-items: center; gap: 0.5rem; }
869
+ .gd-choice[aria-pressed=true] { border-color: var(--action) !important; background: var(--tint) !important; color: var(--accent); }
870
+ .gd-choice:disabled { opacity: 0.6; cursor: default; }
871
+ .gd-note { width: 100%; margin-top: 0.625rem; min-height: 4.5rem; border-radius: 0.75rem; border: 1px solid var(--hair); background: var(--tile); padding: 0.625rem 0.75rem; resize: vertical; }
872
+ .gd-tell { margin-top: 0.5rem; }
873
+ .gd-bars { display: grid; gap: 0.875rem; }
874
+ .gd-bars .br { display: grid; gap: 0.375rem; }
875
+ .gd-bars .lab { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 0.625rem; font-size: 0.9375rem; font-weight: 600; }
876
+ .gd-bars .lab small { font-weight: 500; color: var(--muted); font-size: 0.875rem; text-align: right; }
877
+ .gd-bars .lab .gv { font-variant-numeric: tabular-nums; white-space: nowrap; }
878
+ .gd-bars .pair { display: grid; gap: 0.3125rem; }
879
+ .gd-track { position: relative; height: 0.875rem; border-radius: 0.4375rem; background: var(--sunk); }
880
+ .gd-track.thin { height: 0.5rem; border-radius: 0.25rem; }
881
+ .gd-track i { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: var(--faint); }
882
+ .gd-track i.bad { background: var(--bad); } .gd-track i.warn { background: var(--warn); } .gd-track i.ok { background: var(--ok); } .gd-track i.ghost { opacity: 0.55; }
883
+ .gd-chain { list-style: none; margin: 0; padding: 0; }
884
+ .gd-chain li { display: grid; grid-template-columns: 1.75rem 1fr; gap: 0.75rem; position: relative; padding-bottom: 1rem; }
885
+ .gd-chain li::before { content: ""; position: absolute; left: 0.8125rem; top: 1.75rem; bottom: 0; width: 2px; background: var(--hair); }
886
+ .gd-chain li:last-child { padding-bottom: 0; } .gd-chain li:last-child::before { display: none; }
887
+ .gd-chain .nd { width: 1.75rem; height: 1.75rem; border-radius: 50%; display: grid; place-items: center; background: var(--sunk); color: var(--muted); font-size: 0.8125rem; font-weight: 700; z-index: 1; }
888
+ .gd-chain .nd.bad { background: var(--bad); color: var(--on-accent); } .gd-chain .nd.warn { background: var(--warn); color: var(--on-accent); } .gd-chain .nd.ok { background: var(--ok); color: var(--on-accent); }
889
+ .gd-chain .tx { padding-top: 0.125rem; line-height: 1.4; } .gd-chain .tx small { display: block; color: var(--muted); font-size: 0.875rem; }
890
+ .gd-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.375rem, 1fr)); gap: 0.625rem; }
891
+ .gd-tile { padding: 0.75rem; border-radius: 0.875rem; background: var(--tile); border: 1px solid var(--hair2); display: grid; gap: 0.25rem; align-content: start; }
892
+ .gd-tile > .gd-chip { justify-self: start; }
893
+ .gd-tile b { font-size: 0.9375rem; line-height: 1.3; } .gd-tile span.gt { font-size: 0.875rem; color: var(--muted); line-height: 1.35; }
894
+ .gd-tiles > .gd-tile:last-child:nth-child(odd) { grid-column: 1/-1; }
895
+ .gd-rows { list-style: none; margin: 0 0 0.375rem; padding: 0; border-radius: 1rem; border: 1px solid var(--hair2); overflow: hidden; }
896
+ .gd-rows > li { border-bottom: 1px solid var(--hair2); }
897
+ .gd-rows > li:last-child { border-bottom: 0; }
898
+ .gd-rows .rowbtn, .gd-rows .rowstat { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: 0.125rem 0.75rem; align-items: center; text-align: left; padding: 0.75rem 0.875rem; min-height: 3.5rem; }
899
+ .gd-rows .rowbtn:hover { background: var(--sunk); }
900
+ .gd-rows .ti { font-weight: 600; line-height: 1.3; display: flex; align-items: baseline; gap: 0.5rem; }
901
+ .gd-rows .sm { display: block; font-weight: 400; color: var(--muted); font-size: 0.9375rem; line-height: 1.35; }
902
+ .gd-rows .ic.gchev { transition: transform 0.2s; color: var(--faint); }
903
+ .gd-rows .rowbtn[aria-expanded=true] .ic.gchev { transform: rotate(180deg); }
904
+ .gd-rows .dotk { width: 0.625rem; height: 0.625rem; border-radius: 50%; background: var(--faint); flex: none; }
905
+ .gd-rows .dotk.bad { background: var(--bad); } .gd-rows .dotk.ok { background: var(--ok); } .gd-rows .dotk.warn { background: var(--warn); }
906
+ .gd-rows .body { padding: 0 0.875rem 0.5rem; }
907
+ .gd-rows .body[hidden] { display: none; }
908
+ .gd-rows .body p { margin: 0 0 0.625rem; }
909
+ .gd-opts { display: grid; gap: 0.625rem; margin: 0 0 0.75rem; }
910
+ .gd-opt { display: grid; grid-template-columns: 1.5rem 1fr; gap: 0.75rem; align-items: start; text-align: left; padding: 0.875rem; border-radius: 1rem; border: 1.5px solid var(--hair) !important; background: var(--tile) !important; width: 100%; }
911
+ .gd-opt .rdo { width: 1.375rem; height: 1.375rem; border-radius: 50%; border: 2px solid var(--hair); margin-top: 1px; display: grid; place-items: center; }
912
+ .gd-opt[aria-checked=true] { border-color: var(--action) !important; background: var(--tint) !important; }
913
+ .gd-opt[aria-checked=true] .rdo { border-color: var(--action); }
914
+ .gd-opt[aria-checked=true] .rdo::after { content: ""; width: 0.625rem; height: 0.625rem; border-radius: 50%; background: var(--action); }
915
+ .gd-opt:disabled { cursor: default; }
916
+ .gd-opt .ol { font-weight: 600; line-height: 1.3; display: flex; flex-wrap: wrap; gap: 0.375rem 0.5rem; align-items: center; }
917
+ .gd-opt .oo { display: block; color: var(--muted); font-size: 0.9375rem; line-height: 1.4; margin-top: 0.125rem; }
918
+ .gd-pick { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; color: var(--accent); background: var(--tint); padding: 0.1875rem 0.5rem; border-radius: 999px; }
919
+ .gd-why { padding: 0.75rem 0.875rem; border-radius: 0.875rem; background: var(--well); font-size: 0.9375rem; line-height: 1.45; margin: 0 0 0.625rem; }
920
+ .gd-why b { font-weight: 600; }
921
+ .gd-quiet { display: flex; gap: 0.5rem; align-items: flex-start; font-size: 0.9375rem; color: var(--muted); line-height: 1.4; margin: 0 0 0.75rem; }
922
+ .gd-quiet .ic { width: 1.125rem; height: 1.125rem; margin-top: 0.125rem; }
923
+ .gd-checks { list-style: none; margin: 0 0 0.875rem; padding: 0; display: grid; gap: 0.625rem; }
924
+ .gd-checks li { display: grid; grid-template-columns: 1.5rem 1fr; gap: 0.75rem; align-items: start; }
925
+ .gd-checks .tk { width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--ok-bg); color: var(--ok); display: grid; place-items: center; }
926
+ .gd-checks .tk .ic { width: 0.875rem; height: 0.875rem; stroke-width: 2.8; }
927
+ #reader .gd-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; min-height: 3.125rem; padding: 0 1.5rem; border-radius: 999px; background: var(--action) !important; color: var(--on-accent); font-weight: 600; font-size: 1.0625rem; }
928
+ .gd-btn.block { width: 100%; }
929
+ .gd-btn:disabled { opacity: 0.45; cursor: default; }
930
+ .gd-sent { padding: 1.125rem; border-radius: 1.125rem; background: var(--ok-bg); }
931
+ .gd-sent h3 { margin: 0 0 0.25rem; font-size: 1.25rem; }
932
+ .gd-sent p { margin: 0 0 0.25rem; }
933
+ .gd-err { color: var(--danger); font-size: 0.9375rem; margin: 0.5rem 0 0; }
934
+ .gd-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; padding: 0.625rem 1rem calc(0.625rem + env(safe-area-inset-bottom)); background: color-mix(in srgb, var(--page) 90%, transparent); -webkit-backdrop-filter: saturate(180%) blur(16px); backdrop-filter: saturate(180%) blur(16px); border-top: 1px solid var(--hair2); display: flex; align-items: center; gap: 0.75rem; }
935
+ .gd-bar .txt { flex: 1; min-width: 0; font-size: 0.9375rem; line-height: 1.3; color: var(--muted); }
936
+ .gd-bar .txt b { display: block; color: var(--text); font-weight: 600; }
937
+ .gd-bar .gd-btn { min-height: 3rem; padding: 0 1.25rem; white-space: nowrap; }
938
+ @media (prefers-reduced-motion: reduce) { #reader .gd *, #reader .gd-scroll { animation: none !important; transition: none !important; scroll-behavior: auto; } }
763
939
  </style>
764
940
  </head>
765
941
  <body>
@@ -1416,7 +1592,7 @@
1416
1592
  return a === 'reports' && !state.readonly && d.type !== 'explainer' && !reads(d) ? 'notes' : a;
1417
1593
  };
1418
1594
  /** A page to open: an .html view, framed in a sandbox. */
1419
- const reads = (d) => /\.html$/i.test(d.view ?? '');
1595
+ const reads = (d) => /\.html$/i.test(d.view ?? '') || !!d.guided;
1420
1596
  const hhmm = (at) => new Date(at).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
1421
1597
  /** Where an item opens: its own area, and inside Waiting on you a plan or a design has its own screen; every other ask opens on Decide. */
1422
1598
  const askHref = (d) => {
@@ -1859,10 +2035,279 @@
1859
2035
 
1860
2036
  /** The report a #reports/<key> link opens (or the plan a #waiting/<key> link opens), over the whole board: a bar with the way back and its title, then its page in a sandboxed frame.
1861
2037
  * The frame is made again only for another page, so a redraw never resets a demo halfway through. */
2038
+ /* ---- A guided plan (plan 2) or report (report 3): one scroll, built from the message's fields. ----
2039
+ Read receipts and Go deeper are real: a part read for a moment is saved on the board (/api/reads), and Go deeper asks the agent (/api/deeper),
2040
+ whose answer (sign-off-axi deeper) is polled and shown under the part that was tapped. */
2041
+ const GSTAGES = { this: 'What this is', understood: 'What I understood', found: 'What I found', recommend: 'What I recommend', decide: 'What you decide', next: 'Next step' };
2042
+ const GICONS = { chev: '<path d="m6 9 6 6 6-6"/>', check: '<path d="m5 12.5 4.5 4.5L19 7.5"/>', clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
2043
+ deeper: '<path d="M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-9l-4 3.5V16H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/>', send: '<path d="M21 3 10 14M21 3l-6.5 18-3.5-7.5L3.5 10z"/>', info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>' };
2044
+ const gic = (n, c = '') => { const s = el('span', { 'aria-hidden': 'true', style: 'display:inline-flex' }); s.innerHTML = `<svg class="ic ${c}" viewBox="0 0 24 24">${GICONS[n]}</svg>`; return s; };
2045
+ const E = (tag, cls, ...kids) => el(tag, cls ? { class: cls } : {}, ...kids);
2046
+ const gnum = (n) => String(Number.isInteger(n) ? n : +n.toFixed(2));
2047
+ async function postJson(path, body) {
2048
+ const res = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
2049
+ const out = await res.json().catch(() => ({ error: `The server replied ${res.status}` }));
2050
+ if (!res.ok) throw new Error(out.error);
2051
+ state = out;
2052
+ }
2053
+
2054
+ function guideView(d) {
2055
+ const plan = d.type === 'plan', key = d.key, sections = d.sections ?? [], decisions = plan ? d.decisions ?? [] : [], by = (stage) => sections.filter((s) => s.stage === stage);
2056
+ const live = !FROZEN && !state.readonly;
2057
+ const stages = ['this', 'understood', by('found').length && 'found', by('recommend').length && 'recommend', decisions.length && 'decide', 'next'].filter(Boolean);
2058
+ const saved0 = answerOf(key);
2059
+ const sentNote = (saved0?.notes ?? []).find((n) => n.section === 'understood');
2060
+ const S = { understood: sentNote ? (sentNote.verdict === 'approve' ? 'yes' : 'no') : null, note: sentNote?.note ?? '', picks: Object.fromEntries(decisions.map((q) => [q.key, saved0?.decisions?.[q.key] ?? q.recommend])), told: false };
2061
+ const sent = () => answerOf(key)?.answer === 'sent';
2062
+ const parts = new Map(); // part id -> its card
2063
+ const stageOf = new Map(); // part id -> stage
2064
+ const rcs = new Map(), panels = new Map(), deeps = new Map();
2065
+ const readAt = (part) => state.reads?.[`${key}/${part}`];
2066
+ const changes = () => (S.understood === 'no' ? 1 : 0) + decisions.filter((q) => S.picks[q.key] !== q.recommend).length;
2067
+
2068
+ // ---- receipts: who read this part and when
2069
+ const rd = (cls) => E('i', `gdot ${cls ?? ''}`);
2070
+ function receipt(part) {
2071
+ const at = readAt(part), seen = state.seen_at?.[key], mine = answerOf(key);
2072
+ const you = at ? E('span', '', rd(), E('b', '', 'Read by you'), ` ${hhmm(at)}`) : E('span', '', rd('hollow'), 'You have not read this yet');
2073
+ const told = part === 'understood' && !plan && (state.messages ?? []).filter((m) => m.key === key && m.text.startsWith(NOTOFF)).at(-1);
2074
+ let agent;
2075
+ if (plan && mine?.answer === 'sent' && ((part === 'understood' && sentNoteNow()) || part === 'next' || part.startsWith('d:'))) {
2076
+ const what = part === 'understood' ? 'note' : 'answers';
2077
+ agent = seen ? E('span', '', rd('agent'), E('b', '', `Agent read your ${what}`), ` ${hhmm(seen)}`) : E('span', '', rd('hollow'), `Agent has not read your ${what} yet`);
2078
+ } else if (told) agent = told.delivered ? E('span', '', rd('agent'), E('b', '', 'Agent read your note'), ` ${hhmm(told.delivered)}`) : E('span', '', rd('hollow'), 'Agent has not read your note yet');
2079
+ else agent = E('span', '', rd('agent'), E('b', '', 'Agent wrote this'), d.filed ? ` ${hhmm(d.filed)}` : '');
2080
+ return [you, agent];
2081
+ }
2082
+ const NOTOFF = 'What I understood is off: ';
2083
+ const sentNoteNow = () => (answerOf(key)?.notes ?? []).some((n) => n.section === 'understood' && n.note);
2084
+ const rc = (part, small) => { const box = E('div', `gd-rc${small ? ' s' : ''}`); rcs.set(part + (small ? '~s' : ''), box); return box; };
2085
+ const paintRc = () => rcs.forEach((box, k) => box.replaceChildren(...receipt(k.replace(/~s$/, ''))));
2086
+
2087
+ // ---- Go deeper: the person's ask, the agent's answer streaming in under the part
2088
+ const asksOf = (part) => (state.deeper ?? []).filter((x) => x.key === key && x.part === part);
2089
+ const unfinished = () => (state.deeper ?? []).some((x) => x.key === key && !x.done);
2090
+ function paintAns(part) {
2091
+ const panel = panels.get(part), asks = asksOf(part), sig = JSON.stringify(asks);
2092
+ if (!panel || panel.dataset.sig === sig) return;
2093
+ panel.dataset.sig = sig;
2094
+ const blocks = asks.map((a) => {
2095
+ const waiting = !a.read, writing = !!a.text && !a.done, late = waiting && Date.now() - Date.parse(a.at) > 60_000;
2096
+ const head = a.done ? [E('i', 'dot'), `Agent answered ${hhmm(a.answered)}`] : writing ? [E('i', 'dot'), 'Agent is answering'] : waiting ? [E('i', 'dot'), 'Waiting for the agent to read it'] : [E('i', 'dot'), `Agent read it ${hhmm(a.read)}, working on it`];
2097
+ return E('div', `gask${a.done ? '' : ' live'}`, E('div', 'hd', ...head), E('p', 'q', a.q ? `You asked: ${a.q}` : `Go deeper: ${a.about}`),
2098
+ ...(a.text ? a.text.split(/\n+/).map((t, i, all) => E('p', '', t, ...(writing && i === all.length - 1 ? [E('span', 'caret')] : []))) : [E('div', 'shim', ''), E('div', 'shim', '')]),
2099
+ E('div', 'gd-rc s', E('span', '', rd(), E('b', '', 'You asked'), ` ${hhmm(a.at)}`), a.read ? E('span', '', rd('agent'), E('b', '', 'Agent read it'), ` ${hhmm(a.read)}`) : E('span', '', rd('hollow'), 'Agent has not read it yet')),
2100
+ ...(late ? [E('p', 'slow', 'The agent has not picked this up yet. It answers the next time it checks the board.')] : []));
2101
+ });
2102
+ const done = asks.length && asks.every((a) => a.done);
2103
+ const form = el('form', { class: 'gd-fu' }), input = el('input', { 'aria-label': 'Ask a follow-up', placeholder: 'Ask a follow-up', maxlength: '500' }), go = el('button', { type: 'submit' }, 'Ask');
2104
+ form.append(input, go);
2105
+ form.onsubmit = (e) => { e.preventDefault(); const q = input.value.trim(); if (q) ask(part, q); };
2106
+ panel.replaceChildren(...blocks, ...(done && live ? [form] : []));
2107
+ }
2108
+ async function ask(part, q) {
2109
+ const panel = panels.get(part);
2110
+ try { await postJson('/api/deeper', { key, part, ...(q && { q }) }); sync(); } catch (e) { panel.append(E('p', 'gd-err', `Could not ask: ${e.message}`)); }
2111
+ }
2112
+ const deepBtn = (part) => {
2113
+ const b = el('button', { class: 'gd-lnk deep', type: 'button', 'aria-expanded': 'false', 'aria-controls': `gd-a-${part.replace(/\W/g, '-')}` }, gic('deeper'), 'Go deeper');
2114
+ deeps.set(part, b);
2115
+ b.onclick = () => {
2116
+ const p = panels.get(part), open = b.getAttribute('aria-expanded') === 'true';
2117
+ b.setAttribute('aria-expanded', String(!open)); p.hidden = open;
2118
+ if (!open && !asksOf(part).length) ask(part);
2119
+ };
2120
+ return b;
2121
+ };
2122
+ const ansPanel = (part) => { const p = el('div', { class: 'gd-panel gd-ans', id: `gd-a-${part.replace(/\W/g, '-')}`, hidden: '', 'aria-live': 'polite' }); panels.set(part, p); return p; };
2123
+ const detailsBtn = (part, rows) => {
2124
+ const id = `gd-d-${part.replace(/\W/g, '-')}`, b = el('button', { class: 'gd-lnk', type: 'button', 'aria-expanded': 'false', 'aria-controls': id }, 'Details', gic('chev', 'gchev'));
2125
+ const p = el('div', { class: 'gd-panel', id, hidden: '' }, el('table', { class: 'gd-dt' }, el('caption', { style: 'position:absolute;width:1px;height:1px;overflow:hidden' }, 'The numbers'),
2126
+ ...rows.map((r) => el('tr', {}, el('th', { scope: 'row' }, r.label), el('td', {}, r.value)))));
2127
+ b.onclick = () => { const open = b.getAttribute('aria-expanded') !== 'true'; b.setAttribute('aria-expanded', String(open)); p.hidden = !open; };
2128
+ return [b, p];
2129
+ };
2130
+ const acts = (part, rows) => { const [db, dp] = rows?.length ? detailsBtn(part, rows) : []; return [E('div', 'gd-acts', ...(db ? [db] : []), ...(live ? [deepBtn(part)] : [])), ...(dp ? [dp] : []), ...(live ? [ansPanel(part)] : [])]; };
2131
+
2132
+ // ---- the picture
2133
+ function visual(v) {
2134
+ let body;
2135
+ if (v.type === 'bars') {
2136
+ const pair = v.rows.some((r) => r.before !== undefined), max = Math.max(...v.rows.flatMap((r) => [r.value, r.before ?? 0])) * 1.04 || 1, u = v.unit ? ` ${v.unit}` : '';
2137
+ body = E('div', 'gd-bars', ...v.rows.map((r) => E('div', 'br', E('div', 'lab', E('span', '', r.label), E('small', '', r.note ? `${r.note} · ` : '', E('span', 'gv', pair && r.before !== undefined ? `${gnum(r.before)}${u} to ${gnum(r.value)}${u}` : `${gnum(r.value)}${u}`))),
2138
+ pair ? E('div', 'pair', E('div', 'gd-track thin', el('i', { class: 'ghost', style: `width:${((r.before ?? 0) / max) * 100}%` })), E('div', 'gd-track', el('i', { class: r.tone ?? '', style: `width:${(r.value / max) * 100}%` })))
2139
+ : E('div', 'gd-track', el('i', { class: r.tone ?? '', style: `width:${(r.value / max) * 100}%` })))));
2140
+ } else if (v.type === 'steps') {
2141
+ body = E('ol', 'gd-chain', ...v.rows.map((r, i) => E('li', '', E('span', `nd ${r.tone ?? ''}`, String(i + 1)), E('div', 'tx', ...(r.note ? [E('small', '', r.note)] : []), r.label, ...(r.text ? [E('small', '', r.text)] : [])))));
2142
+ } else body = E('div', 'gd-tiles', ...v.rows.map((r) => E('div', 'gd-tile', ...(r.note ? [E('span', `gd-chip ${r.tone ?? ''}`, r.note)] : []), E('b', '', r.label), ...(r.text ? [E('span', 'gt', r.text)] : []))));
2143
+ return E('figure', 'gd-fig', body);
2144
+ }
2145
+
2146
+ // ---- cards
2147
+ const card = (part, stage, ...kids) => { const c = el('section', { class: 'gd-sec', 'data-part': part, id: `gd-${part.replace(/\W/g, '-')}` }, ...kids); parts.set(part, c); stageOf.set(part, stage); return c; };
2148
+ const head = (h, lead, kicker) => el('div', { class: 'gh' }, ...(kicker ? [E('span', 'gk', kicker)] : []), E('h2', '', h), ...(lead ? [E('p', 'gd-lead', lead)] : []));
2149
+ const hero = card('this', 'this', head(d.visual.heading, d.visual.text), visual(d.visual), ...acts('this', d.visual.details), rc('this'));
2150
+
2151
+ const noteBox = el('textarea', { class: 'gd-note', 'aria-label': plan ? 'What is off' : 'What is off, for the agent', placeholder: 'What is off? One line is enough.', maxlength: '500', hidden: '' });
2152
+ noteBox.value = S.note;
2153
+ const tell = el('button', { class: 'gd-lnk gd-tell', type: 'button', hidden: '' }, 'Tell the agent');
2154
+ const choices = [['yes', 'That is right', true], ['no', 'Not quite']].map(([v, label, tick]) => {
2155
+ const b = el('button', { class: 'gd-choice', type: 'button', 'aria-pressed': 'false', 'data-u': v }, ...(tick ? [gic('check')] : []), label);
2156
+ b.onclick = () => { S.understood = S.understood === v ? null : v; syncU(); paintSummary(); };
2157
+ return b;
2158
+ });
2159
+ noteBox.oninput = () => { S.note = noteBox.value; if (!plan) syncU(); };
2160
+ tell.onclick = async () => {
2161
+ const err = tell.parentElement.querySelector('.gd-err'); err?.remove(); tell.disabled = true;
2162
+ try { await postJson('/api/messages', { kind: 'comment', key, text: `${NOTOFF}${S.note.trim()}` }); S.told = true; syncU(); sync(); } catch (e) { tell.after(E('p', 'gd-err', `Could not send: ${e.message}`)); tell.disabled = false; }
2163
+ };
2164
+ function syncU() {
2165
+ const lock = sent() || !live;
2166
+ choices.forEach((b) => { b.setAttribute('aria-pressed', String(S.understood === b.dataset.u)); b.disabled = lock; });
2167
+ noteBox.hidden = S.understood !== 'no'; noteBox.disabled = lock;
2168
+ tell.hidden = plan || S.understood !== 'no' || !S.note.trim() || S.told || !live; tell.disabled = false;
2169
+ }
2170
+ const understood = card('understood', 'understood', head('Check that I have this right', 'This is your ask in my words. If it is off, say what is off. That is the only correction that matters.'),
2171
+ el('dl', { class: 'gd-uq' }, ...d.understood.map((u) => E('div', '', el('dt', {}, u.label), el('dd', {}, u.text)))),
2172
+ E('div', 'gd-seg', ...choices), noteBox, tell, ...(live ? acts('understood') : []), rc('understood'));
2173
+
2174
+ const secCard = (s) => {
2175
+ const items = (s.items ?? []).map((it, i) => {
2176
+ if (!it.more) return E('li', '', E('div', 'rowstat', E('span', '', E('span', 'ti', ...(it.tone ? [E('i', `dotk ${it.tone}`)] : []), it.title), E('span', 'sm', it.text))));
2177
+ const part = `${s.id}:${i}`, body = E('div', 'body', E('p', '', it.more), ...acts(part), ...[]);
2178
+ body.hidden = true;
2179
+ const b = el('button', { class: 'rowbtn', type: 'button', 'aria-expanded': 'false' }, E('span', '', E('span', 'ti', ...(it.tone ? [E('i', `dotk ${it.tone}`)] : []), it.title), E('span', 'sm', it.text)), gic('chev', 'gchev'));
2180
+ b.onclick = () => { const open = b.getAttribute('aria-expanded') !== 'true'; b.setAttribute('aria-expanded', String(open)); body.hidden = !open; };
2181
+ return E('li', '', b, body);
2182
+ });
2183
+ return card(s.id, s.stage, head(s.heading, s.body), ...(items.length ? [E('ul', 'gd-rows', ...items)] : []), ...acts(s.id, s.details), rc(s.id));
2184
+ };
2185
+
2186
+ const optBtns = new Map();
2187
+ const decCard = (q, n) => {
2188
+ const opts = q.options.map((o) => {
2189
+ const b = el('button', { class: 'gd-opt', type: 'button', role: 'radio', 'aria-checked': 'false' }, E('span', 'rdo'),
2190
+ E('span', '', E('span', 'ol', o.label, ...(o.value === q.recommend ? [E('span', 'gd-pick', 'My pick')] : [])), E('span', 'oo', o.outcome)));
2191
+ b.onclick = () => { S.picks[q.key] = o.value; paintOpts(); paintSummary(); };
2192
+ (optBtns.get(q.key) ?? optBtns.set(q.key, []).get(q.key)).push([o.value, b]);
2193
+ return b;
2194
+ });
2195
+ return card(`d:${q.key}`, 'decide', head(q.question, q.why, `Decision ${n + 1} of ${decisions.length}`), E('div', 'gd-opts', ...opts),
2196
+ ...(q.reason ? [E('div', 'gd-why', E('b', '', 'Why my pick: '), q.reason)] : []), ...(q.quiet ? [E('p', 'gd-quiet', gic('clock'), E('span', '', `If you stay quiet: ${q.quiet}`))] : []), ...acts(`d:${q.key}`), rc(`d:${q.key}`));
2197
+ };
2198
+ const paintOpts = () => {
2199
+ const lock = sent() || !live;
2200
+ optBtns.forEach((list, k) => list.forEach(([v, b]) => { b.setAttribute('aria-checked', String(S.picks[k] === v)); b.disabled = lock; }));
2201
+ };
2202
+
2203
+ // ---- next step and Go ahead
2204
+ const sendSlot = E('div', ''), err = E('p', 'gd-err');
2205
+ err.setAttribute('role', 'alert');
2206
+ const nextCard = card('next', 'next', head(d.next.heading), E('ul', 'gd-checks', ...d.next.lines.map((l) => E('li', '', E('span', 'tk', gic('check')), E('span', '', l)))), ...(plan ? [sendSlot, err] : []), rc('next'));
2207
+ async function go(b) {
2208
+ if (S.understood === 'no' && !S.note.trim()) { noteBox.focus(); noteBox.scrollIntoView({ block: 'center' }); err.textContent = 'Say what is off first, or choose That is right.'; return; }
2209
+ err.textContent = ''; b.disabled = true;
2210
+ const notes = S.understood ? [{ section: 'understood', verdict: S.understood === 'yes' ? 'approve' : 'change', ...(S.note.trim() && { note: S.note.trim() }) }] : [];
2211
+ try { await postJson('/api/answers', { key, note: '', notes, decisions: S.picks }); sync(); } catch (e) { err.textContent = `Couldn't send: ${e.message}`; b.disabled = false; }
2212
+ }
2213
+ const goBtn = () => { const b = el('button', { class: 'gd-btn block', type: 'button' }, gic('send'), changes() ? `Send ${plural(changes(), 'change')}` : 'Go ahead'); b.onclick = () => go(b); return b; };
2214
+
2215
+ // ---- rail, progress and the phone bar
2216
+ const sumHost = E('div', 'gd-sum'), bar = E('div', 'gd-bar'), barTxt = E('div', 'txt'), nav = el('nav', { 'aria-label': 'Parts' }), segs = E('div', 'gd-segs');
2217
+ const now = E('div', 'now', E('span', '', E('b', '')), E('span', ''));
2218
+ stages.forEach((st, i) => {
2219
+ const a = el('a', { href: `#gd-st-${st}`, 'data-nav': st }, E('span', 'gn', String(i + 1)), E('span', 'gt', GSTAGES[st]));
2220
+ a.onclick = (e) => { e.preventDefault(); stageEl.get(st)?.scrollIntoView({ block: 'start' }); };
2221
+ nav.append(a);
2222
+ const s = el('button', { type: 'button', 'aria-label': GSTAGES[st] }); s.onclick = () => stageEl.get(st)?.scrollIntoView({ block: 'start' });
2223
+ segs.append(s);
2224
+ });
2225
+ const stageEl = new Map();
2226
+ const summaryRows = () => [['Understood', S.understood === 'yes' ? 'Right' : S.understood === 'no' ? E('span', 'chg', 'Not quite') : 'My reading stands'],
2227
+ ...decisions.map((q, i) => [q.question, S.picks[q.key] === q.recommend ? 'My pick' : E('span', 'chg', q.options.find((o) => o.value === S.picks[q.key])?.label ?? S.picks[q.key])])];
2228
+ function paintSummary() {
2229
+ const gone = sent(), n = changes();
2230
+ if (plan) {
2231
+ const sendBtn = el('button', { class: 'gd-btn block', type: 'button' }, gic('send'), gone ? 'Sent' : n ? `Send ${plural(n, 'change')}` : 'Go ahead');
2232
+ sendBtn.disabled = gone || !live; sendBtn.onclick = () => go(sendBtn);
2233
+ sumHost.replaceChildren(E('h4', '', 'Your answers'), E('ul', '', ...summaryRows().map(([k, v]) => E('li', '', E('span', '', k), E('span', '', v)))), sendBtn);
2234
+ const b2 = el('button', { class: 'gd-btn', type: 'button' }, gic('send'), gone ? 'Sent' : n ? `Send ${plural(n, 'change')}` : 'Go ahead');
2235
+ b2.disabled = gone || !live; b2.onclick = () => go(b2);
2236
+ barTxt.replaceChildren(...(gone ? [E('b', '', 'Sent'), 'Agent is on it'] : n ? [E('b', '', plural(n, 'change')), 'My picks for the rest'] : [E('b', '', 'My picks are set'), decisions.length ? `${plural(decisions.length, 'question')}, nothing required` : 'Nothing is required']));
2237
+ bar.replaceChildren(barTxt, b2);
2238
+ const mine = answerOf(key), seen = state.seen_at?.[key];
2239
+ sendSlot.replaceChildren(...(gone
2240
+ ? [E('div', 'gd-sent', E('h3', '', 'Sent. I am on it.'), E('p', '', mine.notes?.length || decisions.some((q) => mine.decisions?.[q.key] !== q.recommend) ? 'Your changes are with the agent.' : 'Your answers are my picks.'),
2241
+ E('p', 'gd-cap', seen ? `Agent read your answers ${hhmm(seen)}` : 'Agent has not read it yet'))]
2242
+ : [goBtn(), E('p', 'gd-cap', n ? 'I use my picks for everything you did not change.' : 'Nothing is required. This accepts my picks.')]));
2243
+ }
2244
+ }
2245
+ function paintRail() {
2246
+ const readAll = (st) => [...parts].filter(([id]) => stageOf.get(id) === st).every(([id]) => readAt(id));
2247
+ nav.querySelectorAll('a').forEach((a) => { const on = readAll(a.dataset.nav); a.classList.toggle('seen', on); a.querySelector('.gn').replaceChildren(on ? gic('check') : String(stages.indexOf(a.dataset.nav) + 1)); });
2248
+ [...segs.children].forEach((b, i) => b.classList.toggle('on', readAll(stages[i])));
2249
+ }
2250
+ let cur = stages[0];
2251
+ function setStage(st) {
2252
+ cur = st; const i = stages.indexOf(st);
2253
+ nav.querySelectorAll('a').forEach((a) => a.classList.toggle('cur', a.dataset.nav === st));
2254
+ [...segs.children].forEach((b, k) => b.classList.toggle('cur', k === i));
2255
+ now.firstChild.firstChild.textContent = GSTAGES[st]; now.lastChild.textContent = `${i + 1} of ${stages.length}`;
2256
+ }
2257
+
2258
+ // ---- the page
2259
+ const cards = [hero, understood, ...by('found').map(secCard), ...by('recommend').map(secCard), ...decisions.map(decCard), nextCard];
2260
+ const stageBlocks = [];
2261
+ let last = '';
2262
+ for (const c of cards) {
2263
+ const st = stageOf.get(c.dataset.part);
2264
+ if (st !== last) { last = st; const s = el('div', { class: 'gd-stage', id: `gd-st-${st}`, 'data-stage': st }, E('i', '', String(stages.indexOf(st) + 1)), GSTAGES[st]); stageEl.set(st, s); stageBlocks.push(s); }
2265
+ stageBlocks.push(c);
2266
+ }
2267
+ const word = plan ? 'Plan' : 'Report', who = plan ? 'Planning agent' : d.asked_by ?? 'Reporting agent';
2268
+ const top = E('div', 'gd-top', E('p', 'geye', [word, plan ? d.project : d.group, [who, d.filed && when(d.filed)].filter(Boolean).join(', ')].filter(Boolean).join(' · ')), E('h1', '', d.title), E('p', 'sub', d.about),
2269
+ E('div', 'howto', gic('info'), E('span', '', `A few short parts, in order. Tap Details for the numbers and Go deeper to ask me more.${plan ? ' Nothing is required: Go ahead accepts my picks.' : ''}`)));
2270
+ const rail = el('aside', { class: 'gd-rail' }, E('h4', '', plan ? 'This plan' : 'This report'), nav, ...(plan ? [sumHost] : []));
2271
+ const progress = E('div', 'gd-progress', now, segs);
2272
+ const main = el('div', { class: 'gd-main' }, top, ...stageBlocks);
2273
+ const scroll = E('div', 'gd-scroll', E('div', 'gd', progress, E('div', 'gd-cols', rail, main)));
2274
+ const root = E('div', '', scroll, ...(plan ? [bar] : []));
2275
+ root.style.cssText = 'display:flex;flex-direction:column;flex:1;min-height:0';
2276
+
2277
+ // ---- reading: a part on screen for a moment is read
2278
+ const mark = (part) => { if (!live || readAt(part) !== undefined) return; (state.reads ??= {})[`${key}/${part}`] = new Date().toISOString(); paintRc(); paintRail(); postJson('/api/reads', { key, part }).then(sync).catch(() => {}); };
2279
+ const timers = new Map();
2280
+ const quick = matchMedia('(prefers-reduced-motion: reduce)').matches;
2281
+ const io = new IntersectionObserver((es) => es.forEach((en) => { const p = en.target.dataset.part; if (en.isIntersecting) timers.set(p, setTimeout(() => mark(p), 1400)); else clearTimeout(timers.get(p)); }), { root: scroll, threshold: 0.35 });
2282
+ parts.forEach((c) => io.observe(c));
2283
+ const io2 = new IntersectionObserver((es) => es.forEach((en) => { if (en.isIntersecting) setStage(en.target.dataset.stage); }), { root: scroll, rootMargin: '-30% 0px -60% 0px' });
2284
+ stageEl.forEach((s) => io2.observe(s));
2285
+ parts.forEach((c) => c.dataset.stage = stageOf.get(c.dataset.part));
2286
+ parts.forEach((c) => io2.observe(c));
2287
+
2288
+ // ---- keeping the page true to the board
2289
+ let timer;
2290
+ function sync() {
2291
+ if (!root.isConnected && timer) { clearInterval(timer); timer = undefined; return; }
2292
+ syncU(); paintOpts(); paintSummary(); paintRc(); paintRail();
2293
+ panels.forEach((_, part) => { paintAns(part); if (asksOf(part).length && deeps.get(part).getAttribute('aria-expanded') === 'false' && !deeps.get(part).dataset.shut) { deeps.get(part).setAttribute('aria-expanded', 'true'); panels.get(part).hidden = false; } });
2294
+ const need = live && (unfinished() || (plan && sent() && !state.seen_at?.[key]) || (!plan && S.told && !(state.messages ?? []).some((m) => m.key === key && m.text.startsWith(NOTOFF) && m.delivered)));
2295
+ if (need && !timer) timer = setInterval(async () => {
2296
+ if (!root.isConnected) { clearInterval(timer); timer = undefined; return; }
2297
+ try { const res = await fetch('/api/decisions'); if (res.ok) { state = await res.json(); sync(); } } catch {}
2298
+ }, 1500);
2299
+ else if (!need && timer) { clearInterval(timer); timer = undefined; }
2300
+ }
2301
+ root.sync = sync;
2302
+ deeps.forEach((b) => b.addEventListener('click', () => { b.dataset.shut = b.getAttribute('aria-expanded') === 'true' ? '' : '1'; }));
2303
+ setStage(stages[0]);
2304
+ queueMicrotask(sync);
2305
+ return root;
2306
+ }
1862
2307
  const reader = document.getElementById('reader');
1863
2308
  function readPage(d, from = 'reports') {
1864
2309
  // A design or a report takes the board's place beside the sidebar on a desk; a plan, and anything on a phone, opens over the whole board.
1865
- const pane = !!d && d.type !== 'plan', inline = pane && matchMedia('(min-width: 60rem)').matches;
2310
+ const pane = !!d && d.type !== 'plan' && !d.guided, inline = pane && matchMedia('(min-width: 60rem)').matches;
1866
2311
  document.body.classList.toggle('reading', !!d);
1867
2312
  document.body.classList.toggle('pane-reading', pane);
1868
2313
  reader.setAttribute('role', inline ? 'region' : 'dialog');
@@ -1871,11 +2316,11 @@
1871
2316
  document.getElementById('nav').inert = !!d && !inline;
1872
2317
  document.getElementById('tabs').inert = !!d;
1873
2318
  reader.hidden = !d;
1874
- const url = d?.kind === 'design' ? `design:${d.key}` : d?.type === 'evidence' ? `evidence:${d.key}` : d ? src(mockUrl(d.key, '', d.view)) : '';
1875
- const answer = d?.type === 'plan' || d?.kind === 'design' || d?.type === 'evidence' ? JSON.stringify(state.answers[d.key] ?? null) : '';
1876
- const plan = d?.type === 'plan' ? JSON.stringify(d) : '';
2319
+ const url = d?.guided ? `guide:${d.key}` : d?.kind === 'design' ? `design:${d.key}` : d?.type === 'evidence' ? `evidence:${d.key}` : d ? src(mockUrl(d.key, '', d.view)) : '';
2320
+ const answer = d?.guided ? '' : d?.type === 'plan' || d?.kind === 'design' || d?.type === 'evidence' ? JSON.stringify(state.answers[d.key] ?? null) : '';
2321
+ const plan = d?.type === 'plan' || d?.guided ? JSON.stringify(d) : '';
1877
2322
  const evidence = d?.type === 'evidence' ? JSON.stringify([d, (state.messages ?? []).filter((m) => m.key === d.key), (state.rounds ?? []).filter((r) => r.key === d.key)]) : '';
1878
- if (reader.dataset.url === url && reader.dataset.answer === answer && reader.dataset.plan === plan && reader.dataset.evidence === evidence) return d && (reader.querySelector('h1').textContent = d.type === 'evidence' ? headline(d) : d.title);
2323
+ if (reader.dataset.url === url && reader.dataset.answer === answer && reader.dataset.plan === plan && reader.dataset.evidence === evidence) return d && (reader._guide?.sync(), 1) && (reader.querySelector('h1').textContent = d.type === 'evidence' ? headline(d) : d.title);
1879
2324
  Object.assign(reader.dataset, { answer, plan, evidence });
1880
2325
  Object.assign(reader.dataset, { url, key: d?.key ?? '', from });
1881
2326
  if (!d) return reader.replaceChildren();
@@ -1885,7 +2330,8 @@
1885
2330
  if (change) { const s = el('span', { 'aria-hidden': 'true' }); s.innerHTML = '<svg viewBox="0 0 24 24"><path d="M14 3h7v7M21 3l-9 9M19 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/></svg>'; out.append(s); }
1886
2331
  // A report's page reads under its own head: what it is and when, its title, what it says and decided; a plan or a design keeps its title in the bar;
1887
2332
  // an update shows its own title on its screen, with the change link in the bar.
1888
- reader.replaceChildren(...(d.type === 'plan' ? [el('header', {}, back, Object.assign(h1, { className: 'plan-name' })), planReview(d, url)] : d.kind === 'design' ? [el('header', {}, back), designReview(d, h1)]
2333
+ reader._guide = undefined;
2334
+ reader.replaceChildren(...(d.guided ? [el('header', {}, back), (reader._guide = guideView(d))] : d.type === 'plan' ? [el('header', {}, back, Object.assign(h1, { className: 'plan-name' })), planReview(d, url)] : d.kind === 'design' ? [el('header', {}, back), designReview(d, h1)]
1889
2335
  : d.type === 'evidence' ? [el('header', {}, back, out), evidenceItem(d)]
1890
2336
  : [el('header', {}, back), el('div', { class: 'read' }, el('p', { class: 'eyebrow' }, [kind(d) || 'Report', d.filed ? when(d.filed) : ''].filter(Boolean).join(' · ')), h1,
1891
2337
  d.about ? el('p', { class: 'about', dir: 'auto' }, d.about) : '', decided(d), el('iframe', { src: url, title: `Report: ${d.title}`, sandbox: 'allow-scripts' }), dismissReport(d))]));
@@ -1953,7 +2399,7 @@
1953
2399
  function planCard(d) {
1954
2400
  const meta = [el('span', {}, icon('list'), plural(d.sections.length, 'section')), el('span', {}, icon('split'), plural(d.decisions.length, 'decision')),
1955
2401
  el('span', {}, icon('clock'), `Planning agent${d.filed ? `, ${when(d.filed)}` : ''}`)];
1956
- return el('article', { class: 'plan-hero', id: `item-${d.key}` }, el('div', { class: 'plan-preview' }, rung(d)),
2402
+ return el('article', { class: 'plan-hero', id: `item-${d.key}` }, ...(d.guided ? [] : [el('div', { class: 'plan-preview' }, rung(d))]),
1957
2403
  el('div', { class: 'plan-copy' }, el('p', { class: 'eyebrow' }, icon('plan'), `Plan${d.project ? ` · ${d.project}` : ''}`),
1958
2404
  el('h2', { dir: 'auto' }, d.title), el('p', { class: 'about', dir: 'auto' }, d.about ?? ''),
1959
2405
  el('div', { class: 'plan-meta' }, ...meta), el('div', { class: 'plan-actions' }, el('a', { class: 'open-link plan-open', href: `#waiting/${d.key}` }, 'Open plan'), xBtn(d, 'Dismiss'))));
@@ -2128,7 +2574,7 @@
2128
2574
  el('div', { class: 'tiles' }, tile('waiting', by.waiting.length, 'Need you'), tile('evidence', by.provenToday.length, 'Proven today'), tile('reports', filedToday(by.reports).length, 'New report')),
2129
2575
  el('div', { class: 'areas' }, waitingSection(by.waiting, by.answered, by.designs), proven(by.evidence, by.provenToday), newReport(by.reports)), ...(state.leash ? leash() : []),
2130
2576
  ...(FROZEN || state.readonly || state.answers_only ? [] : [ask()])]));
2131
- readPage(key ? state.decisions.find((d) => d.key === key && !gone.has(d.key) && (at === 'reports' && reads(d) || at === 'designs' && d.kind === 'design' || at === 'evidence' && d.type === 'evidence' || d.type === 'plan' && area(d) === at)) : undefined, at);
2577
+ readPage(key ? state.decisions.find((d) => d.key === key && !gone.has(d.key) && (at === 'reports' && reads(d) || at === 'designs' && d.kind === 'design' || at === 'evidence' && d.type === 'evidence' || d.type === 'plan' && (area(d) === at || (d.guided && (at === 'waiting' || at === 'answered'))))) : undefined, at);
2132
2578
  if (at === 'decide') document.querySelector('#board .decide')?.prepend(bulkBar(bulk));
2133
2579
  applyPicks();
2134
2580
  if (FROZEN) freeze();
@@ -0,0 +1,99 @@
1
+ {
2
+ "contract": 1,
3
+ "kind": "plan",
4
+ "schema": 2,
5
+ "key": "FILL: short-id",
6
+ "title": "FILL: the plan's answer in one line",
7
+ "project": "FILL: project",
8
+ "about": "FILL: the plan in 2 sentences, answer first",
9
+ "understood": [
10
+ {
11
+ "label": "You asked",
12
+ "text": "FILL: the request in your own words"
13
+ },
14
+ {
15
+ "label": "Done when",
16
+ "text": "FILL: how the person will know it is finished"
17
+ }
18
+ ],
19
+ "visual": {
20
+ "type": "bars",
21
+ "heading": "FILL: the one point the picture makes, in one line",
22
+ "text": "FILL: what the bars measure and where the number comes from",
23
+ "unit": "min",
24
+ "rows": [
25
+ {
26
+ "label": "FILL: first thing",
27
+ "value": 4.5,
28
+ "note": "FILL: tag",
29
+ "tone": "bad"
30
+ },
31
+ {
32
+ "label": "FILL: second thing",
33
+ "value": 2.8,
34
+ "note": "FILL: tag",
35
+ "tone": "ok"
36
+ }
37
+ ],
38
+ "details": [
39
+ {
40
+ "label": "FILL: a number",
41
+ "value": "FILL: its value, unit and when it was measured"
42
+ }
43
+ ]
44
+ },
45
+ "sections": [
46
+ {
47
+ "id": "steps",
48
+ "stage": "recommend",
49
+ "heading": "FILL: what you recommend, as a statement",
50
+ "body": "FILL: one or two lines of lead",
51
+ "items": [
52
+ {
53
+ "title": "FILL: step or fix",
54
+ "text": "FILL: one line",
55
+ "more": "FILL: why, and what it costs"
56
+ },
57
+ {
58
+ "title": "FILL: step or fix",
59
+ "text": "FILL: one line"
60
+ }
61
+ ],
62
+ "details": [
63
+ {
64
+ "label": "FILL: a number",
65
+ "value": "FILL: value"
66
+ }
67
+ ]
68
+ }
69
+ ],
70
+ "decisions": [
71
+ {
72
+ "key": "FILL: short-id",
73
+ "question": "FILL: one question, 12 words or fewer?",
74
+ "why": "FILL: why it needs an answer now",
75
+ "options": [
76
+ {
77
+ "value": "a",
78
+ "label": "FILL: first choice",
79
+ "outcome": "FILL: what happens if picked"
80
+ },
81
+ {
82
+ "value": "b",
83
+ "label": "FILL: second choice",
84
+ "outcome": "FILL: what happens if picked"
85
+ }
86
+ ],
87
+ "recommend": "a",
88
+ "reason": "FILL: why you would pick the first",
89
+ "quiet": "FILL: what you do if the person stays quiet"
90
+ }
91
+ ],
92
+ "next": {
93
+ "heading": "FILL: the next step, in one line",
94
+ "lines": [
95
+ "FILL: what happens after the person reads or answers",
96
+ "FILL: where the proof will land"
97
+ ]
98
+ }
99
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "contract": 1,
3
+ "kind": "report",
4
+ "schema": 3,
5
+ "key": "FILL: short-id",
6
+ "title": "FILL: the answer in one line",
7
+ "filed": "FILL: UTC time",
8
+ "about": "FILL: why it matters, in 2 sentences",
9
+ "group": "FILL: section name",
10
+ "understood": [
11
+ {
12
+ "label": "You asked",
13
+ "text": "FILL: the request in your own words"
14
+ },
15
+ {
16
+ "label": "Done when",
17
+ "text": "FILL: how the person will know it is finished"
18
+ }
19
+ ],
20
+ "visual": {
21
+ "type": "bars",
22
+ "heading": "FILL: the one point the picture makes, in one line",
23
+ "text": "FILL: what the bars measure and where the number comes from",
24
+ "unit": "min",
25
+ "rows": [
26
+ {
27
+ "label": "FILL: first thing",
28
+ "value": 4.5,
29
+ "note": "FILL: tag",
30
+ "tone": "bad"
31
+ },
32
+ {
33
+ "label": "FILL: second thing",
34
+ "value": 2.8,
35
+ "note": "FILL: tag",
36
+ "tone": "ok"
37
+ }
38
+ ],
39
+ "details": [
40
+ {
41
+ "label": "FILL: a number",
42
+ "value": "FILL: its value, unit and when it was measured"
43
+ }
44
+ ]
45
+ },
46
+ "sections": [
47
+ {
48
+ "id": "found",
49
+ "stage": "found",
50
+ "heading": "FILL: what you found, as a statement",
51
+ "body": "FILL: one or two lines of lead",
52
+ "items": [
53
+ {
54
+ "title": "FILL: one finding",
55
+ "text": "FILL: one line",
56
+ "more": "FILL: the detail that opens when it is tapped",
57
+ "tone": "bad"
58
+ },
59
+ {
60
+ "title": "FILL: another finding",
61
+ "text": "FILL: one line",
62
+ "more": "FILL: the detail",
63
+ "tone": "ok"
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "id": "steps",
69
+ "stage": "recommend",
70
+ "heading": "FILL: what you recommend, as a statement",
71
+ "body": "FILL: one or two lines of lead",
72
+ "items": [
73
+ {
74
+ "title": "FILL: step or fix",
75
+ "text": "FILL: one line",
76
+ "more": "FILL: why, and what it costs"
77
+ },
78
+ {
79
+ "title": "FILL: step or fix",
80
+ "text": "FILL: one line"
81
+ }
82
+ ],
83
+ "details": [
84
+ {
85
+ "label": "FILL: a number",
86
+ "value": "FILL: value"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "next": {
92
+ "heading": "FILL: the next step, in one line",
93
+ "lines": [
94
+ "FILL: what happens after the person reads or answers",
95
+ "FILL: where the proof will land"
96
+ ]
97
+ }
98
+ }