{
  "agent": {
    "accordion": {
      "state": {
        "openItems": "Ids (values) of currently-expanded items. Single-mode accordions return an array with 0 or 1 entries."
      },
      "actions": {
        "expand": "Expand the item with the given value.",
        "collapse": "Collapse the item with the given value.",
        "clear": "Collapse all items. Only meaningful for multi-mode accordions; single-mode equivalent to collapsing the active item."
      }
    },
    "addressAutocomplete": {
      "state": {
        "value": "Current free-text value of the address input.",
        "query": "Current query driving the suggestion listbox.",
        "isOpen": "True when the suggestion listbox is visible."
      },
      "actions": {
        "setValue": "Set the input text programmatically; opens and refreshes the suggestion list (issues an external address-lookup fetch).",
        "applyFilter": "Set the query text, which refreshes the suggestion list via an external address-lookup fetch.",
        "clear": "Clear the typed text and dismiss the suggestion list.",
        "open": "Open the suggestion listbox.",
        "close": "Close the suggestion listbox."
      }
    },
    "addressFields": {
      "state": {
        "value": "The current structured address: { street, city, postcode, province, country, formatted }."
      },
      "actions": {
        "setValue": "Replace the entire address.",
        "setField": "Set a single address field. Setting `country` also clears the now-stale province."
      }
    },
    "agendaTray": {
      "state": {
        "items": "Currently-displayed appointments. Ids + status only — never patient identity."
      },
      "actions": {
        "selectItem": "Activate the appointment with the given id."
      }
    },
    "aiConsentBanner": {
      "state": {
        "consentState": "Current consent state. Structural only — never the grantee identity.",
        "armed": "Whether the consent confirmation checkbox is ticked (required state only)."
      }
    },
    "aiPromptInput": {
      "state": {
        "value": "Current text in the composer (no PHI processing).",
        "isEmpty": "True when the composer has no text and no attachments.",
        "isSubmitting": "True while an onSubmit invocation is in flight (best-effort)."
      },
      "actions": {
        "setValue": "Replace the composer text with the given value.",
        "clear": "Clear text and attachments. Irreversible from the same UI.",
        "submit": "Submit the current composer state via onSubmit.",
        "focus": "Move keyboard focus into the textarea."
      }
    },
    "anamnesisSummaryCard": {
      "state": {
        "summary": "Structural summary of the anamnesis card — section, answer, highlighted and unanswered counts plus the consumer id. Never question labels or answer values."
      }
    },
    "appointmentTray": {
      "state": {
        "items": "Currently-displayed appointments. Ids + lifecycle state only — never patient identity."
      },
      "actions": {
        "selectItem": "Activate the appointment with the given id."
      }
    },
    "appointmentDetailsPanel": {
      "state": {
        "eventId": "The occurrence on screen or being read, as the host identifies it. After a failed or refused read, the occurrence it was for when the host says so. Null when there is none, or the host did not say.",
        "loadStatus": "Where the read is: loading, ready, failed (with Retry) or refused. Actions need ready.",
        "appointmentStatus": "The appointment status as last saved here or read. Null when none is recorded or nothing is on screen.",
        "allowedStatuses": "The statuses set_status accepts now, in menu order. Empty when the status is read-only.",
        "offeredActions": "The footer actions on offer, as their data-action values (for example edit-appointment, delete-appointment), in footer order, including any folded into More.",
        "historyOpen": "Whether the History sub-view covers the details.",
        "privacy": "Whether privacy mode is on. The patient identity and personal fields are then masked on screen."
      },
      "actions": {
        "openHistory": "Slide the appointment's history over the details, as its header button does; the host reads it then if it did not send it with the details. It lists who changed what, and when.",
        "closeHistory": "Return from History to the details.",
        "setStatus": "Save a new appointment status through the host, as the footer control does. Resolves once the host has saved it.",
        "retry": "Ask the host to read the appointment again, after a failed read or a refusal it marked retryable."
      }
    },
    "appointmentTimeline": {
      "state": {
        "items": "Currently-rendered appointments. Ids + lifecycle state only — never patient identity."
      },
      "actions": {
        "selectItem": "Activate the appointment with the given id.",
        "jumpToToday": "Re-anchor the timeline scroll on today's appointments."
      }
    },
    "audioRecorder": {
      "state": {
        "isRecording": "True while actively capturing audio (not paused).",
        "duration": "Elapsed recording time in milliseconds (paused time excluded).",
        "hasRecording": "True when a completed recording is available for submission."
      },
      "actions": {
        "startRecording": "Request microphone permission and begin recording.",
        "stopRecording": "Stop the current recording and finalise the blob.",
        "discard": "Cancel the recording. Irreversible — the captured audio is dropped.",
        "submit": "Stop the active recording, finalising it via onRecordingComplete."
      }
    },
    "autocomplete": {
      "state": {
        "value": "Currently-selected option value, or null when nothing is committed.",
        "query": "Current free-text query in the input.",
        "isOpen": "True when the suggestion popup is visible. Open with no suggestions to show reads false."
      },
      "actions": {
        "setValue": "Set the committed value programmatically.",
        "applyFilter": "Set the input query, which filters the suggestion list.",
        "clear": "Clear the committed value and the query. Discards the typed input.",
        "open": "Open the suggestion list.",
        "close": "Close the suggestion list."
      }
    },
    "bishopScore": {
      "state": {
        "selection": "The five cervical-exam findings keyed by component (dilation, effacement, station, consistency, position). Each value is an enumerated option key, not free text.",
        "result": "Computed Bishop result — total (0–13) and favourability band (unfavourable / intermediate / favourable). Null until all five findings are set."
      },
      "actions": {
        "setValue": "Set one cervical-exam finding to an enumerated option value. Unknown component/value pairs are ignored.",
        "reset": "Clear all five findings and the computed result.",
        "getResult": "Return the computed Bishop result, or null until all five findings are set."
      }
    },
    "bmiCalculator": {
      "state": {
        "unitSystem": "Active unit system the inputs are entered in.",
        "inputs": "Canonical metric measurements regardless of the active unit system — height in centimetres and weight in kilograms, or null when unset.",
        "result": "Computed BMI (kg/m²) and WHO category (underweight / normal / overweight / obese). Null until both height and weight are valid."
      },
      "actions": {
        "setValue": "Set height (cm) and/or weight (kg) in canonical metric units. Omitted fields are left unchanged.",
        "setUnitSystem": "Switch the input unit system. Entered figures are preserved by conversion; the computed BMI is unchanged.",
        "reset": "Clear height and weight and the computed result.",
        "getResult": "Return the computed BMI + WHO category, or null until both inputs are valid."
      }
    },
    "booking": {
      "state": {
        "variant": "Active variant — one of `inline-list`, `accordion`, `calendar`, `flexible`, `progress`.",
        "currentStep": "The step the flow is on: 1 insurance, 2 specialty, 3 service, 4 provider, 5 date, 6 time, 7 patient details, payment or optional SMS code verification, 8 confirmation after all required steps succeed, including the \"Time reserved\" view that opens a host address after a short delay. Read this back after `goto_step` to see where the clamp landed."
      },
      "actions": {
        "reset": "Clear the booking value back to an empty state. Use to abandon an in-progress flow.",
        "gotoStep": "Jump to the given step. A step that is hidden for this practice, or that sits ahead of the first incomplete step, clamps to that first incomplete step — read `currentStep` back to see where it landed."
      }
    },
    "breadcrumb": {
      "state": {
        "items": "Ordered list of crumb ids + hrefs in display order. Labels are intentionally excluded — they may contain PHI."
      },
      "actions": {
        "navigateTo": "Activate the crumb with the given id — equivalent to clicking the link."
      }
    },
    "calendar": {
      "state": {
        "currentView": "Active view name (e.g. dayGridMonth, timeGridWeek).",
        "currentDate": "ISO date anchor of the visible range.",
        "currentTitle": "Human title of the current range (e.g. \"April 2026\")."
      },
      "actions": {
        "today": "Jump to the current day.",
        "prev": "Move to the previous range.",
        "next": "Move to the next range.",
        "gotoDate": "Navigate to a specific date.",
        "changeView": "Switch the calendar to the given view.",
        "focusEvent": "Scroll an event into view and briefly highlight it."
      },
      "workflows": {
        "browseAgenda": "Navigate the agenda: today, previous / next range, jump to a date through the navigation row's date picker, or change what the grid shows from the one view menu — a range (day, three days, week, month, also reachable as the bare keys D · X · W · M) and a column axis (plain, chairs, operators), plus a remembered \"Show weekends\" item that hides Saturday and Sunday in every range. Each range change hands the new window to the host, which fetches the appointments for it.",
        "filterColumns": "Narrow the grid to particular chairs or operators from the two filter popovers, with the platform's semantics: nothing ticked shows everyone and ticking narrows. Operators list under their groups with a tri-state row per group; \"Only available operators\" is on by default in the operator views, and a session-local \"Show disabled operators\" switch reveals the operators the practice has disabled. A count badge rides each filter button while it narrows, and the \"Reset filters\" chip beside them clears both selections and restores both switches without ever changing the view. Selection only — no appointment is touched.",
        "maskPatientNames": "Turn privacy mode on or off from the toolbar. While on, patient identity is masked across the grid and the whole rail: the cards, the hover peek, the Appointments panel, the appointment details panel (patient card, description, booking contact, notes preview and the WhatsApp link are hidden), the online bookings, the waiting list and the printed call sheet; the Notes tool and the edit form show a notice instead of their content, and so does a tool the host adds, unless the host masks it itself. The page can stay open beside a waiting patient. It is a display mask, not a permission: it is not persisted and it changes no record.",
        "configureDisplay": "Set the per-user display knobs in the cog's settings menu (each setting's row shows its current value and opens a submenu of options; on/off settings are switch rows; the last item resets to the defaults) — layout, zebra rows, show times, slot precision, the day window, the care-description mode, what the cards are coloured by, the default duration of a new appointment (a preset from 10 to 60 minutes, or the value already saved), whether operators read as an initials disc or a dot plus full name, whether chairs read as a full name or a short code, whether the details panel offers the map link, the row density (compact or comfortable), the colour strength of the cards (tinted or solid), and whether cards show the operator or chair from the other axis. The same menu holds the appointment icon legend as a submenu. An agent drives the menu with instance id calendar-display-settings: set_value with a choice row's name (orientation, slotDensity, window, slotDuration, eventColorStyle, colorAssociation, careDescription, operatorNameDisplay, chairNameDisplay, defaultDurationMin) and one of its option values, or set_checked with a switch row's name (zebraRows, showTime, showCardContext, mapsLink); the menu opens for the moment when closed. The settings state lists each row's name, value and options while the menu is open. The choices are remembered per user through the host's preferences adapter under `calendarWorkspace.*` keys, so they persist beyond the page load; they change presentation only and touch no patient record.",
        "dockTool": "Open a workspace rail tool: Appointments, appointment editor / details, waiting list, patient notes, online bookings or free-slot finder. Read the actual strip: hosts may withhold tools, and patient-notes is registered only with patient context. Notes uses the host's note permissions, independently of appointment-edit rights. Clearing its patient context returns to the previous available tool and focuses its rail button. Opening notes from a panel may address a note id and moves focus into Notes. Closing other tools returns to Appointments. A host may add its own tools after these; they carry the host's ids, so read the strip's data-tool-id before opening one. open ignores an id no tool carries; select_tool with one, or a host tool the host withdraws while open, docks Appointments. patient-notes is the exception: withheld or without a patient, it returns to the previous tool, unannounced.",
        "reviewUpcoming": "Read the rail's resting panel: the practice's upcoming appointments — now to the end of the seventh day, cancelled excluded, earliest first — each row led by the patient. The preset row, the jump-to-date picker and the edge loaders widen the derived window up to ±180 days. Activating a row selects that appointment on the grid, which docks its details. Read-only; the rows carry ids and lifecycle state, never patient identity.",
        "readAppointmentDetails": "Read one appointment in the docked details panel (appointment-details-panel). With the host's details read, any card click, including a patient-less block and a chip whose names are still loading, focuses the appointment and docks a read-only panel: appointment, booking, money, contact, treatments, signing and series sections, the patient's latest three notes and a History sub-view. See more or a note opens the separate Notes rail tool when the host offers it. While the read is loading, failed or refused the panel shows that status instead. Edit, withheld from a read-only viewer, opens the editor. Without the host's read a pending chip stays inert and the editor form is the details. Reads patient-identifiable data.",
        "createAppointment": "Book a new appointment in the editor panel, docked from the New appointment button, from a click on an empty slot (prefilled with the slot, the clicked column and the cog's default duration) or from an online booking being processed. A host without the tool rail shows the same form in AppointmentEditorDialog, a modal with no rail to open. Set the patient, the linked care or care-plan lines, the date, start time and duration, the operator, chair, branch and colour, the description (255 characters at most — a longer one blocks the save), and, once a patient is chosen, the SMS and e-mail reminders, the first-visit question and — where the patient has unsigned required documents — how they are asked to sign. Repeats sets a frequency and, where the host allows it, a Custom rule (every N days, weeks, months or years; chosen weekdays, always including the first appointment's; or the day or nth weekday of the month) ending after a number of times or on a date. Busy chair, busy operator and operator-away warnings are advisory and do not hold the save; only a practice closure does. Enter submits from any text field. Writes patient-identifiable data.",
        "editAppointment": "Amend an existing appointment in the docked editor. With the host's details read the editor opens from Edit on the details panel and Cancel or Save returns there; without it the details panel is the form. A remote host first loads canonical data; loading, failure and whole-appointment refusal expose no editable form. When that data carries a series, the editor first asks the scope: this occurrence, this and following, or the whole series. Honour the returned patient, care, description and recurrence capabilities. A refused recurring scope cannot be treated as an ordinary edit. Save waits for host acceptance before closing or announcing success; rejection preserves the entered draft for correction or retry. Writes patient-identifiable data.",
        "changeAppointmentStatus": "Set an appointment's attendance status from the status control in the details panel (appointment-details-panel). The workspace offers all six statuses; allowedStatuses lists what set_status accepts. A refusal keeps the previous status and shows the host's message. The status is patient-identifiable attendance data.",
        "moveAppointment": "Reschedule an appointment either by dragging its card to a new slot or by arming \"Move…\" in the details panel and then clicking the target slot — the panel stays docked and a banner tinted with the appointment's own colour names what is being placed. Either route ends in the confirmation dialog, which offers the SMS and e-mail notify channels and, for a series member, the recurring scope. A host recurring parent always confirms, asks the scope and hides the notify channels, as the platform does. Cancelling reverts the card and commits nothing. Writes patient-identifiable data.",
        "resizeAppointment": "Change an appointment's duration by dragging its card edge. Unlike a move this SAVES AT ONCE with no confirmation, exactly as the platform does; the toast that follows carries an Undo that restores the previous duration and logs both changes on the appointment's history. Writes patient-identifiable data.",
        "copyAppointment": "Duplicate an appointment onto another slot: arm \"Copy…\" in the details panel, then click the target. The copy lands immediately with no confirmation, and its toast carries an Undo. A plan-linked appointment first asks whether to copy the appointment only or the appointment and its treatments. Writes patient-identifiable data.",
        "deleteAppointment": "Remove an appointment from the details panel behind an alert dialog. The dialog asks for the recurring scope when the appointment belongs to a series, and for the care-plan cleanup when it is the parent of a price-list recurrence — so confirming can drop far more than the one card. Irreversible from this surface.",
        "managePatientNotes": "Read and write the patient's notes from the conditional Notes rail tool: add a note through the composer, edit one in place, pin or flag one, or delete one behind the panel's alert dialog. Every write goes to the host on the click, and the host stamps the note's id, time and author. The kit attributes nothing to a clinical note. A refused delete, pin or flag is rolled back and explained in the panel. An HTML note edits in a rich text editor and saves as HTML; a clinical or deleted note offers no edit. With no host seam, or when the host's seam is not writable, the tool is a read-only timeline. Explicit note permissions take precedence over appointment-edit permission. Details and the editor have no Notes sub-view. The rows carry ids and pin / flag / clinical markers only; the body and author are never exposed as state. Writes patient-identifiable clinical data.",
        "manageTask": "Create, edit, complete or delete a practice task in the editor tool's slot, in the published task editor. The toolbar's New task member opens an empty task, dated from the grid's selection or now; a click on a task row on the grid opens that task, read through the host. Set the date, the note, the type, the assignee and at most one counterpart (a patient, a company or an operator), then save through the host. Done and Delete act on a saved task at once. Each action is withheld when the host passes no seam for it. A task can link a patient, so writes are patient-identifiable.",
        "readModificationHistory": "Open the History sub-view of the details panel and read the appointment's modification ledger, newest first — what changed, from what to what, and when. No mutation.",
        "requestSignature": "Ask the patient to sign the appointment's documents from the details panel, by SMS or e-mail through the kit menu. The chip beside it reports the channel a request has already gone out on. There is no \"On this device…\" item: on-device signing is the host's own e-signature surface and no seam here mounts it, so the menu offers only the two channels it can honour. Sends a patient-identifiable request.",
        "createDocument": "Open the appointment details create menu through data-action=\"open-create-document\" and select an offered data-action=\"create-document-<kind>\" item: bill, folder, template, empty, identification or webcam. The host supplies permitted kinds and owns navigation, confirmation and persistence. Appointment read-only mode does not withhold document rights. A selection starts the host workflow; it does not prove that a document or bill was created. May write patient-identifiable data.",
        "messagePatientWhatsapp": "Reach the patient over WhatsApp from a waiting-list row. The affordance is a `wa.me` deep link the row opens in a new tab — it is disabled, and names its reason in its accessible name, when the entry carries no WhatsApp-capable number. There is no equivalent in the details panel: the send-reminder group is handed an empty channel list, because the host's legacy Snippets island is the only composing surface and no seam here mounts it. Hands a patient's contact detail to an outbound channel.",
        "filterWaitingList": "Narrow the waiting queue: the four quick views (all, patients without an appointment, appointment scheduled over 30 days ago, on the list over 30 days) and, behind \"Show advanced filters\", the platform's nine optional client-side inputs — patient, patient assigned, operator, appointment scheduled, appointment scheduled from / to, on the waiting list from / to and description — applied only when \"Filter list\" is pressed. The toggle carries a count badge while any input is set, and changing the quick view drops the form. Selection only.",
        "manageWaitingListEntry": "Add, amend or remove a waiting-list entry. The four-field form opens as a rail SUB-VIEW — from the toolbar split button's plus segment, from Add entry, or from a row's pencil — with a back arrow to the queue. Editing is refused on a row already linked to an appointment, because those fields belong to the appointment; the pencil stays focusable and names that reason. Remove deletes at once with no confirmation, platform parity, and its toast carries an Undo that puts the entry back. Writes patient-identifiable data.",
        "scheduleWaitingPatient": "Give a waiting patient a slot. \"Schedule\" on an unlinked row hands off to the free-slot finder seeded from the entry — operator, chair, duration, patient, care and colour already filled in — and booking a slot opens the editor carrying the entry, so saving books the appointment and takes the entry off the list. A row already linked to an appointment reads \"Reschedule\" and seeds the same finder, but booking there MOVES the existing appointment rather than creating a second one. Writes patient-identifiable data.",
        "findFreeSlot": "Search the practice for bookable time in the finder panel. Operator and chair scope are grouped checklists behind a popover per axis — a select-all row first, operators sectioned under their groups, nothing ticked searching every eligible resource — beside the care, the duration, the date range and, for a care that needs one, a medical-device select. The results list groups the slots under a day heading; when the viewer may write appointments, a click steers the grid to that day and docks the editor prefilled with the date, time, duration, chair, operator and any seeded patient or care; read-only viewers see the list and projection only, under a lock note, with no Book. The 185-day range warning is advisory (the search still runs), \"Show more slots\" pushes the To date out by 30 days and re-searches, and an alert names which business hours are missing when the practice or its operators hold none (it links to the scheduler only where the host supplies that navigation, which this workspace does not). Projected bands open a modal list of original options by click, Enter or Space; only an explicit Book action uses the finder booking path. Read-only viewers can scroll those options without Book. Dismissal restores focus to the band.",
        "triageOnlineBooking": "Review the online-bookings queue independently of the visible dates. The host owns membership and loading/error state. Show a row on the grid; completion is available only when the host enables it and the user can edit. Review-only rows remain in the queue on selection and cannot be dragged, resized or edited. Unknown last-seen data never marks a booking New. A host addresses one booking with the focusBookingId prop (bump focusBookingKey to repeat it): the tool docks, then the card carrying that data-booking-id takes focus and aria-current; an id outside the queue addresses nothing. An Assign patient or Assign operator segment opens the editor focused on that field. A settings cog in the panel header (data-backlog-settings) opens a menu (instance id calendar-backlog-settings) of three switch rows — withoutPatient, withoutOperator, withoutChair, the chair one only where the practice has chairs — all on by default, then a Reset row that switches them all back on; set_checked with the name of a row flips it; a card stays listed while at least one of its own gaps is still switched on. Read the cog before reading the list: while any switch is off it carries data-narrowed and its accessible name says the list is filtered, so a short queue is not an empty backlog. Selection only; completing writes patient-identifiable data.",
        "printDaySheet": "Print the front-desk call sheet. The toolbar's printer icon opens a dialog asking which day — one day, never the visible range — and Print renders that day's appointments through the browser, scoped by the filters in force and masked when privacy mode is on. Nothing is sent to a server, but the sheet itself is a patient-identifiable document."
      }
    },
    "carePlanCard": {
      "state": {
        "state": "The plan lifecycle state. Enum value only — never the localised label or any PHI.",
        "progress": "Line progress as done / total counts. Counts only — never treatment names or operator identity."
      }
    },
    "carePlanEntryCard": {
      "state": {
        "done": "Whether the treatment line is marked executed.",
        "phaseProgress": "Phase progress as executed / total counts. Counts only — never phase labels or operator identity."
      }
    },
    "carePlanHeader": {
      "state": {
        "state": "The plan lifecycle state. Enum value only — never the localised label or any PHI.",
        "alertCount": "How many attention alerts (e.g. missing signature) are showing. Count only — never the alert labels."
      }
    },
    "carousel": {
      "state": {
        "activeIndex": "Zero-based index of the currently snapped slide.",
        "count": "Total slide count."
      },
      "actions": {
        "scrollTo": "Scroll to the slide at the given index.",
        "scrollPrev": "Scroll to the previous slide.",
        "scrollNext": "Scroll to the next slide."
      }
    },
    "chart": {
      "state": {
        "declaredType": "The consumer-declared chart type — the shape `update_series` expects.",
        "effectiveType": "The type currently rendered — differs from declaredType after a type-switcher pick.",
        "availableTypes": "Types the switcher offers. Empty when the chart has no type switcher."
      },
      "actions": {
        "changeView": "Switch the rendered chart type via the type switcher. No-op (returns false) when the chart has no switcher or the type is not offered.",
        "updateSeries": "Replace the chart series without remounting. Pass the DECLARED type’s shape (see declaredType state); when the user has switched the view cross-shape the values are converted to the rendered shape. The data and its summary table persist across type switches, resizes and theme changes until the host passes a new series prop.",
        "exportPng": "Return a PNG data URI of the chart for export or screenshotting. Waits until series pushed with update_series are drawn."
      }
    },
    "chatContainer": {
      "state": {
        "isAtBottom": "True when the viewport is pinned to the newest message. The read-back for `scroll_to_bottom` — the direct confirmation that the latest turn is on screen.",
        "messageCount": "Number of messages currently rendered in the log. Count only — never message content."
      },
      "actions": {
        "scrollToBottom": "Scroll the conversation viewport to the latest message. Read the `isAtBottom` slot afterwards to confirm the viewport settled there."
      }
    },
    "chatInput": {
      "state": {
        "value": "Current text in the composer.",
        "isEmpty": "True when the composer has no text."
      },
      "actions": {
        "setValue": "Replace the composer text.",
        "clear": "Clear the composer. Irreversible from the same UI.",
        "submit": "Submit the current composer state via onSubmit.",
        "focus": "Move keyboard focus into the textarea."
      }
    },
    "checkboxGroup": {
      "state": {
        "selection": "Values of currently-checked options."
      },
      "actions": {
        "setSelection": "Replace the current selection with the given option values.",
        "clearSelection": "Uncheck every option in the group."
      }
    },
    "checkbox": {
      "state": {
        "checked": "Current checked state."
      },
      "actions": {
        "setChecked": "Set the checked state to a specific value.",
        "toggle": "Flip the checked state between checked and unchecked."
      }
    },
    "cityInput": {
      "state": {
        "value": "Current city / town value.",
        "isEmpty": "Whether the field has no value."
      },
      "actions": {
        "setValue": "Replace the city value.",
        "clear": "Empty the field. Loses any typed value."
      }
    },
    "clinicalNoteCard": {
      "state": {
        "noteId": "Stable identifier of the rendered note, when supplied.",
        "pinned": "Whether the note is pinned to the top of the timeline.",
        "flagged": "Whether the note is flagged for follow-up.",
        "attachmentKind": "Structural kind the note is attached to. Never the attachment identity or content."
      }
    },
    "collapsible": {
      "state": {
        "isOpen": "True when the content panel is expanded."
      },
      "actions": {
        "toggle": "Toggle between open and closed.",
        "open": "Expand the content panel.",
        "close": "Collapse the content panel."
      }
    },
    "colorPicker": {
      "state": {
        "value": "Currently picked colour as a hex string (e.g. \"#3b82f6\")."
      },
      "actions": {
        "setValue": "Set the picked colour from a hex string."
      }
    },
    "combobox": {
      "state": {
        "value": "Currently-selected option value, or null when nothing is selected.",
        "query": "Current filter query in the combobox input.",
        "isOpen": "True when the option list is visible."
      },
      "actions": {
        "setValue": "Set the selected value programmatically.",
        "applyFilter": "Set the filter query for the option list.",
        "clearSelection": "Clear the current selection.",
        "open": "Open the option list.",
        "close": "Close the option list."
      }
    },
    "commandPalette": {
      "state": {
        "query": "Current filter query in the command palette input.",
        "isOpen": "True when the palette is visible."
      },
      "actions": {
        "open": "Open the command palette.",
        "close": "Close the command palette.",
        "applyFilter": "Set the filter query."
      }
    },
    "connectionStatus": {
      "state": {
        "status": "Connection state of the external service: 'connected', 'disconnected', 'pending', or 'error'."
      },
      "actions": {
        "connect": "Begin connecting the external service — invokes the consumer-supplied onConnect handler.",
        "disconnect": "Disconnect the external service — invokes the consumer-supplied onDisconnect handler."
      }
    },
    "contactProfileCard": {
      "state": {
        "profession": "Profession string the card is currently rendering — undefined when the consumer omitted it."
      }
    },
    "controlRoomRail": {
      "state": {
        "panelMode": "How an open panel shows: docked beside the content, over it because the frame keeps a minimum width for main, or full screen on a phone."
      }
    },
    "copyField": {
      "state": {
        "value": "The value displayed in the read-only field.",
        "copied": "True for the brief window after a successful copy."
      },
      "actions": {
        "copy": "Programmatically trigger the clipboard write."
      }
    },
    "countrySelect": {
      "state": {
        "value": "Selected ISO 3166-1 alpha-2 country code (lowercased), or null when nothing is selected."
      },
      "actions": {
        "setValue": "Set the selected country by ISO alpha-2 code.",
        "clearSelection": "Clear the selected country."
      }
    },
    "cycleCalculator": {
      "state": {
        "inputs": "The three cycle inputs as entered — last-period start date (`yyyy-MM-dd`), average cycle length in days, and period (bleed) length in days. Null where unset.",
        "result": "Computed prediction — ovulation date, fertile window, next period, current bleed range and the next few period starts, all `yyyy-MM-dd`. Null until a last-period date is set."
      },
      "actions": {
        "setLastPeriod": "Set the start date of the last period. This is the only required input — the prediction stays null without it.",
        "setCycleLength": "Set the average cycle length in days. Shifts ovulation, the fertile window and every upcoming period.",
        "setPeriodLength": "Set the period (bleed) length in days. Optional — it only adds the current bleed range to the result."
      }
    },
    "dashboardGrid": {
      "state": {
        "layout": "Current widget geometry in cell units — ids and rectangles only, no widget content.",
        "cols": "Logical column count of the board.",
        "editing": "Whether edit mode is on. Move/resize actions are refused while it is off — the host app owns the toggle.",
        "stacked": "Whether the board is narrower than its stackBelow width and shows its widgets as one read-only column. Move/resize actions are unavailable while it is true, even in edit mode; the saved layout is unchanged.",
        "selectedId": "The widget the host has marked (for example while its settings are open), or null. Set by the host, not by agents.",
        "loadingIds": "Ids of the widgets still waiting for their figures (the host set loading). Their bodies show placeholders, so read their values only once they leave this list. Set by the host, not by agents."
      },
      "actions": {
        "revealWidget": "Scroll a widget into view (its scrolling ancestor and the window) and highlight it briefly. Works in view and edit mode, and for a widget added a moment ago. Does not move focus.",
        "moveWidget": "Move a widget to a cell. Out-of-bounds targets are clamped; colliding widgets are pushed down and the layout re-compacts. Refused while edit mode is off or the board is stacked.",
        "resizeWidget": "Resize a widget to a span. Clamped to the grid and the widget’s min/max; colliding widgets are pushed down. Refused while edit mode is off or the board is stacked."
      }
    },
    "dataTableTabs": {
      "state": {
        "activeTabId": "Id of the currently-active saved-filter tab.",
        "tabs": "The saved-filter tabs, in strip order (ids + labels)."
      },
      "actions": {
        "selectTab": "Activate a saved-filter tab: snapshots the outgoing tab, applies the incoming filter model to the table.",
        "getTabs": "Return the saved-filter tabs (ids + labels) in order.",
        "addTab": "Add a new tab capturing the table's current filter model, and activate it. Returns the new tab id.",
        "renameTab": "Rename a saved-filter tab. No-op on locked tabs and empty labels.",
        "removeTab": "Remove a saved-filter tab (its saved filter is lost). No-op on locked tabs and the last remaining tab."
      }
    },
    "dataTable": {
      "state": {
        "selection": "Row ids of currently-selected rows.",
        "filter": "Current ag-grid filter model, keyed by column id.",
        "sort": "Active sort, in priority order.",
        "currentPage": "Zero-indexed page number.",
        "expandedRowId": "Id of the row whose inline detail is open (expandable row), or null."
      },
      "actions": {
        "selectRows": "Replace the current selection with the given row ids.",
        "selectRange": "Select every row between fromId and toId, inclusive, in current sort order.",
        "clearSelection": "Deselect all rows.",
        "getSelection": "Return the row ids of the currently-selected rows.",
        "applyFilter": "Replace the filter model with the given object.",
        "sortBy": "Replace the active sort with the given list (priority order).",
        "goToPage": "Navigate to the given zero-indexed page."
      }
    },
    "datePicker": {
      "state": {
        "value": "Currently-selected ISO date, or null.",
        "isOpen": "Whether the calendar popover is open."
      },
      "actions": {
        "setDate": "Select a specific date (ISO yyyy-mm-dd).",
        "clear": "Clear the selected date.",
        "open": "Open the calendar popover.",
        "close": "Close the calendar popover."
      }
    },
    "dateRangePicker": {
      "state": {
        "range": "Currently-selected ISO date range endpoints, each possibly null.",
        "isOpen": "Whether the range calendar popover is open.",
        "granularity": "Selected time granularity (Range Cockpit), or null when the control is disabled.",
        "compareRange": "Resolved comparison period when compare-to-previous-period is on, else null."
      },
      "actions": {
        "setRange": "Replace the selected range with the given ISO endpoints.",
        "clear": "Clear the selected range.",
        "open": "Open the range calendar popover.",
        "close": "Close the range calendar popover.",
        "apply": "Commit the current draft range (as the Apply / Done button).",
        "cancel": "Discard the draft and close without committing.",
        "setGranularity": "Set the time granularity (Range Cockpit).",
        "toggleCompare": "Turn compare-to-previous-period on or off (Range Cockpit)."
      }
    },
    "dateTimePicker": {
      "state": {
        "value": "Currently-selected ISO datetime, or null.",
        "isOpen": "Whether the datetime popover is open."
      },
      "actions": {
        "setDatetime": "Select a specific datetime (ISO string).",
        "clear": "Clear the selected datetime.",
        "open": "Open the datetime popover.",
        "close": "Close the datetime popover."
      }
    },
    "dependentSelector": {
      "state": {
        "value": "Id of the currently selected person, or empty string when none."
      },
      "actions": {
        "setValue": "Select the person row with the given id."
      }
    },
    "dialog": {
      "state": {
        "isOpen": "True when the dialog is currently open."
      },
      "actions": {
        "open": "Open the dialog.",
        "close": "Close the dialog. Reversible by reopening.",
        "dismiss": "Dismiss the dialog. Loses any in-progress input that has not been committed."
      }
    },
    "documentPreview": {
      "state": {
        "kind": "Which render branch is active: pdf, image, metadata (no inline preview), or empty (no document). Structural — never the document name."
      },
      "actions": {
        "getName": "Read the display name of the previewed document. May identify a patient (PHI)."
      }
    },
    "dropdownMenu": {
      "state": {
        "isOpen": "True when the menu is currently open.",
        "settings": "The menu's named settings rows and their current values: choice rows with the selected value and every option's value, switch rows with whether they are on. Rows are listed only while the menu is open; open it first to read them."
      },
      "actions": {
        "open": "Open the menu.",
        "close": "Close the menu.",
        "setValue": "Pick an option on a named choice row (ChoiceSub), as clicking it would. A closed menu opens for the moment and closes again.",
        "setChecked": "Turn a named switch row (SwitchItem) on or off, as clicking it would. A closed menu opens for the moment and closes again."
      }
    },
    "dueDateCalculator": {
      "state": {
        "inputs": "The dating inputs as entered — method, its reference date, cycle length (LMP), embryo age (IVF), the optional CRL + scan date, and the evaluation date. Dates are `yyyy-MM-dd`. Read this to see which method a reference date will be interpreted under.",
        "result": "Computed estimated delivery date, the GA day-0 anchor after any CRL re-dating, the gestational age as of the evaluation date, and the trimester band. Null until a reference date is set."
      },
      "actions": {
        "setMethod": "Choose which anchor the pregnancy is dated from. The reference date is kept and re-interpreted under the new method, so set the method first.",
        "setReferenceDate": "Set the reference date for the active method. This is the only required input — the result stays null without it.",
        "setCycleLength": "LMP method only — set the average cycle length, which shifts Naegele dating off the 28-day assumption.",
        "setEmbryoAge": "IVF method only — set the embryo age at transfer, which offsets the transfer date back to a notional LMP.",
        "setCrl": "LMP method only — set the first-trimester crown-rump length and its scan date. When the two disagree with the LMP beyond the ACOG threshold, the scan re-dates the pregnancy and overrides cycle length.",
        "setAsOfDate": "Set the date gestational age, trimester and the antenatal schedule are evaluated against. Defaults to today; the estimated delivery date itself does not depend on it."
      }
    },
    "emailInput": {
      "state": {
        "value": "The current email address typed into the field.",
        "isValid": "True when the value is non-empty AND parses as a valid email per RFC 5322."
      }
    },
    "entitySummary": {
      "state": {
        "sections": "Structural section list — ids, fixed category headings, tone and item counts. Never the entity name or the bullet items (both PHI).",
        "generatedAtIso": "ISO-8601 instant the brief was generated, when known."
      },
      "actions": {
        "regenerate": "Request the host to regenerate the AI brief. No-op when the regenerate affordance is not available.",
        "close": "Dismiss the summary panel. No-op when the close affordance is not available (e.g. docked in the rail)."
      }
    },
    "faceScheme": {
      "state": {
        "activeView": "Id of the currently displayed view (front / profile-left / profile-right).",
        "zoom": "Current figure zoom factor (1 = the base framing; up to 8). Only meaningful when the surface enables `zoomable`.",
        "chart": "Current face chart — the finding types recorded on each zone, keyed by zone id. Free-text notes and the patient photo are not exposed here.",
        "markedZones": "Zone ids carrying at least one finding.",
        "activeTool": "The active free-form drawing tool (marker / pen / highlighter / arrow / line / rect / ellipse / text), or null when none is selected.",
        "annotationCounts": "Count of free-form annotations on the active view, keyed by kind (marker / pen / highlighter / arrow / line / rect / ellipse / text). Never includes coordinates or the free-text of a text callout.",
        "selectedAnnotation": "The selected free-form annotation on the active view (id + kind), or null when nothing is selected. Never includes coordinates or the free-text of a text callout.",
        "selectedZones": "Zone ids in the multi-selection (zoneSelect=\"multi\") — the set a bulk finding apply targets. Empty when the mode is off or nothing is selected.",
        "annotationZones": "Count of free-form annotations on the active view keyed by their linked zone id ('unassigned' = outside every zone or not yet assessed). Counts only — never coordinates or text."
      },
      "actions": {
        "focusZone": "Move focus to a specific face zone by id.",
        "getZone": "Read the finding types recorded on one zone (notes are not returned).",
        "zonesWith": "List the zone ids carrying a given finding type.",
        "setFinding": "Record a finding on a zone, with an optional free-text note.",
        "removeFinding": "Remove a single finding from a zone.",
        "clearZone": "Clear every finding from a zone.",
        "autoAlign": "Run the on-device detector to align the zone overlay to the patient photo (works on the active view — front or profile). No-op if detection is disabled or no face is found.",
        "setView": "Switch the active view ('front' / 'profile-left' / 'profile-right'). Each view renders a different zone set, so switch before focusing a zone that belongs to another view.",
        "setZoom": "Set the figure zoom factor (1 = reset to the base framing, clamped to 1–8), anchored on the window centre. No-op unless the surface enables `zoomable`.",
        "setTool": "Select the active free-form drawing tool ('none' turns drawing off; pointer then falls through to zone selection).",
        "undoAnnotation": "Remove the most-recently-added free-form annotation from the active view.",
        "selectAnnotation": "Select a free-form annotation on the active view by id (null clears). Selection reveals the mark and enables its panel affordances (delete / recolour).",
        "setAnnotationZone": "Link a free-form annotation to a face zone by id (null unlinks it). Unknown annotation ids are ignored. New drawings are auto-linked to the zone under them; this re-assigns marks drawn outside every zone.",
        "selectZones": "Replace the zone multi-selection (zoneSelect=\"multi\"). Selection is UI state only — pair with set_finding_bulk to record a finding on the selected zones, or pass ids straight to set_finding_bulk instead.",
        "setFindingBulk": "Record one finding on several zones in a single chart commit (the multi-zone diagnosis path). Zones already carrying the finding are left as they are; the optional note lands only where the finding is newly added.",
        "clearAnnotations": "Clear every free-form annotation from the active view. Zone findings are untouched."
      }
    },
    "fetalWeight": {
      "state": {
        "biometry": "The four ultrasound measurements as entered, in millimetres — biparietal diameter, head circumference, abdominal circumference and femur length. Null where unset. Read this to see why a result is still null.",
        "result": "Estimated fetal weight in grams and which Hadlock regression produced it — `hadlock4` when all four measurements are present, `hadlock1` from AC + FL alone. Null until AC and FL are both set."
      },
      "actions": {
        "setBiometry": "Set any subset of the ultrasound biometry in millimetres. Omitted measurements are left unchanged; an explicit null clears one. AC and FL are the minimum for an estimate."
      }
    },
    "fieldArray": {
      "state": {
        "count": "Number of rows currently rendered."
      },
      "actions": {
        "add": "Append a new row.",
        "remove": "Remove the row at the given index."
      }
    },
    "fileManager": {
      "state": {
        "selection": "Item ids of the currently-selected files and folders.",
        "currentFolder": "Id of the folder being viewed, or null for the root.",
        "typeFilter": "Active document-type filter applied to the open folder (empty = no filter).",
        "items": "Structural metadata for every item — ids, kinds, sizes, dates, document types, signature states. Names are PHI and never included; read them via get_item_names.",
        "activeAction": "The single in-flight action dialog. Only one action can be active at a time."
      },
      "actions": {
        "selectItems": "Replace the current selection with the given item ids.",
        "clearSelection": "Deselect every item.",
        "navigate": "Open the given folder (null for the root). Clears the current selection.",
        "setTypeFilter": "Filter the open folder to the given document types (empty clears the filter).",
        "openItem": "Open an item: folders navigate into the folder, files resolve through the host preview callback.",
        "listItems": "List structural metadata (ids, kinds, sizes, dates — never names) for a folder or the whole tree.",
        "getItemNames": "Read the display names for the given item ids. Names may identify patients (PHI)."
      }
    },
    "fileUpload": {
      "state": {
        "files": "Files currently tracked by the uploader. Identifiers and metadata only — never file content."
      },
      "actions": {
        "removeFile": "Remove the file with the given id from the upload list.",
        "clear": "Remove all files from the upload list."
      }
    },
    "fiscalCodeInput": {
      "state": {
        "value": "Current codice fiscale, normalised (uppercased, 0–16 [A-Z0-9] characters).",
        "isValid": "Whether the value is 16 structurally-sound characters with a matching control character."
      },
      "actions": {
        "setValue": "Replace the codice fiscale. The value is uppercased and stripped to [A-Z0-9], capped at 16 characters.",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the codice fiscale input."
      }
    },
    "freemiumPaywall": {
      "state": {
        "isOpen": "True when the paywall dialog is open.",
        "selectedPlanId": "Opaque id of the currently-selected plan."
      },
      "actions": {
        "upgrade": "Fire onUpgrade with the currently-selected plan id.",
        "dismiss": "Dismiss the paywall via the Not Now action. Irreversible from the same UI."
      }
    },
    "gestationalAgeCalculator": {
      "state": {
        "inputs": "The dating inputs as entered — whether the reference date is a last period or a known due date, that date, and the evaluation date. Dates are `yyyy-MM-dd`. Read this to see which method a reference date will be interpreted under.",
        "result": "Computed gestational age in completed weeks + days as of the evaluation date, the trimester band (preconception / first / second / third / postterm), and the estimated delivery date. Null until a reference date is set."
      },
      "actions": {
        "setMethod": "Choose how the reference date is read. The date is kept and re-interpreted under the new method, so set the method first — an EDD may be in the future, an LMP may not.",
        "setReferenceDate": "Set the dating anchor. This is the only required input — the result stays null without it.",
        "setAsOfDate": "Set the date gestational age, trimester and the antenatal schedule are evaluated against. Defaults to today; the estimated delivery date itself does not depend on it."
      }
    },
    "hcgDoubling": {
      "state": {
        "samples": "The two timed β-hCG samples as entered — value in mIU/mL plus the draw time as an ISO-8601 timestamp. Null where unset. Read this to see why a result is still null.",
        "result": "Computed doubling time in hours (null when not rising), the rise normalised to 48 hours, and the trend band against the minimal-rise benchmark. Null until both samples are set and the second is after the first."
      },
      "actions": {
        "setSamples": "Set any subset of the two timed β-hCG samples. Omitted keys are left unchanged; an explicit null clears one. All four are needed before a trend is computed."
      }
    },
    "headerSettings": {
      "state": {
        "locale": "Active IETF language tag (e.g. \"en\", \"it\", \"ar\"). Initially uses HTML lang first when followHtmlLang is enabled; otherwise storage, then HTML lang when initialLocaleFromHtmlLang is enabled, then the browser. Read after set_locale to confirm the switch; unknown tags leave it unchanged.",
        "theme": "Stored base-theme preference. `'system'` means no override is set and the OS decides — read `resolvedTheme` for what is actually painted.",
        "accessibility": "Stored AAA-contrast preference. `'system'` means no override is set and `prefers-contrast` decides.",
        "resolvedTheme": "The theme actually applied to the document once preference and OS signals resolve. This is what makes a `set_theme(\"system\")` or `set_accessibility(\"system\")` write verifiable."
      },
      "actions": {
        "setLocale": "Switch the active locale to the given IETF tag. Unknown tags are silently dropped — read the `locale` state slot afterwards to confirm. On a site with one address per language, the host may then load that language's address.",
        "setTheme": "Switch the theme preference. Verify with the `theme` slot (the stored preference) and `resolvedTheme` (what is actually painted).",
        "setAccessibility": "Switch the accessibility preference. Verify with the `accessibility` slot (the stored preference) and `resolvedTheme` (what is actually painted)."
      }
    },
    "heatmap": {
      "state": {
        "selection": "Row and column ids of the selected cells.",
        "selectionMode": "How cells can be selected in this heatmap.",
        "view": "Whether the heatmap or its table view is showing.",
        "sortRows": "Current row order.",
        "rowIds": "Row ids in display order, every group expanded. Opaque, never a label.",
        "columnIds": "Column ids in order. Opaque, never a label.",
        "groupIds": "Group ids in display order, empty unless rows are grouped. A group not listed in `groups` shows as group-1, group-2 and so on.",
        "collapsedGroups": "Ids (as in groupIds) of the row groups that are collapsed."
      },
      "actions": {
        "selectCell": "Make one cell the whole selection.",
        "selectCells": "Replace the selection with the given cells.",
        "selectRange": "Select the rectangle of cells between two corners, in the rows as shown.",
        "clearSelection": "Deselect every cell.",
        "readCell": "Read a cell's value, count, status and colour step (no labels).",
        "toggleTableView": "Switch between the heatmap and its table view.",
        "setView": "Show the heatmap or its table view.",
        "sortRows": "Reorder the rows.",
        "expandGroup": "Show a group's rows.",
        "collapseGroup": "Hide a group's rows, leaving its totals row."
      }
    },
    "hoverCard": {
      "state": {
        "isOpen": "True when the hover card is currently open."
      },
      "actions": {
        "open": "Open the hover card without moving focus into it.",
        "close": "Close the hover card."
      }
    },
    "ibanInput": {
      "state": {
        "value": "Current IBAN, normalised (uppercased, [A-Z0-9], no spaces, ≤34 characters).",
        "isValid": "Whether the value is a structurally complete IBAN with a passing mod-97-10 checksum."
      },
      "actions": {
        "setValue": "Replace the IBAN. The value is uppercased and stripped to [A-Z0-9], capped at 34 characters.",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the IBAN input."
      }
    },
    "iconPicker": {
      "state": {
        "value": "Id of the currently selected icon, or an empty string when nothing is picked.",
        "isOpen": "True when the picker popover is currently open.",
        "query": "Current search query filtering the icon grid by label."
      },
      "actions": {
        "setValue": "Select the icon with the given id.",
        "open": "Open the picker popover.",
        "close": "Close the picker popover.",
        "filter": "Filter the icon grid to labels containing the query."
      }
    },
    "imageCropper": {
      "state": {
        "crop": "Current crop rectangle in normalised [0,1] coordinates: { x, y, width, height }."
      },
      "actions": {
        "setCrop": "Set the crop rectangle (normalised [0,1]). Values are clamped to the image bounds and the minimum crop size.",
        "reset": "Reset the crop to the centred default for the current aspect."
      }
    },
    "infoTip": {
      "state": {
        "isOpen": "True when the info tip card is currently open."
      },
      "actions": {
        "open": "Open the info tip card (including its formula and Learn more link, when set).",
        "close": "Close the info tip card."
      }
    },
    "list": {
      "state": {
        "selection": "Ids of currently-selected list items. Empty array when none selected or list is presentational."
      },
      "actions": {
        "setSelection": "Replace the current selection with the given item ids. No-op on presentational lists.",
        "clearSelection": "Deselect all items."
      }
    },
    "localePicker": {
      "state": {
        "locale": "Resolved IETF tag of the locale currently applied to the page. Read this after `set_locale` — the setter silently drops tags the kit ships no bundle for, so this is the only confirmation the switch landed.",
        "isSystemDefault": "True when following navigator.language. False for a saved or explicit choice, or when initialLocaleFromHtmlLang or followHtmlLang used the page language. Read alongside locale to distinguish browser detection from a concrete language selection."
      },
      "actions": {
        "setLocale": "Switch the active locale to the given IETF tag. Unsupported tags are a silent no-op — read the `locale` state slot afterwards to confirm the switch landed."
      }
    },
    "mapView": {
      "state": {
        "consentGranted": "True when the GDPR consent gate has been passed. Read this FIRST: while it is false every action is a silent no-op against a placeholder, and nothing else in the surface says so. When it is true but `center` is still null, Google Maps is loading; wait for `center` before acting.",
        "markerCount": "Number of markers the consumer passed. Zero means there is nothing for `select_marker` to select. A count only — no ids, labels or positions.",
        "selectedMarkerId": "Id of the marker the host currently holds as selected, or null. Selection is fully controlled by the consumer, so `select_marker` only proposes — this slot is the only way to see whether the proposal was accepted.",
        "center": "Live map centre in lat/lng, or null until the map has loaded (consent not granted, or Google Maps still loading). The read-back for `pan_to`, and the sign that the map is ready.",
        "zoom": "Live zoom level, or null until the map has loaded. Google clamps zoom to the map type's range, so this is not necessarily the value last passed to `set_zoom`."
      },
      "actions": {
        "panTo": "Pan the map to a new centre without changing zoom. Read the `center` slot back to confirm — Google animates the pan, so the applied centre settles after the call.",
        "setZoom": "Set the map zoom level (typical range 1–20). Read the `zoom` slot back — Google clamps the request to the map type’s supported range, so requested and applied can differ.",
        "selectMarker": "Propose a marker selection by id, or pass null to clear it. Fires the host's `onMarkerClick`; selection is controlled by the host, so read the `selectedMarkerId` slot back to see whether it accepted."
      }
    },
    "messageTray": {
      "state": {
        "items": "Currently-displayed messages. Ids + read flag only — no PHI / no sender names.",
        "unreadCount": "Badge unread count surfaced by the host.",
        "isOpen": "Whether the dropdown panel is currently open."
      },
      "actions": {
        "open": "Open the message panel.",
        "close": "Close the message panel.",
        "selectItem": "Activate the message with the given id.",
        "markRead": "Request the host to mark a message as read.",
        "dismiss": "Request the host to dismiss / archive the message with the given id.",
        "markAllRead": "Request the host to mark every message as read.",
        "viewAll": "Navigate to the full inbox / message list."
      }
    },
    "multiSelect": {
      "state": {
        "selection": "Values of currently-selected options.",
        "query": "Current filter query.",
        "isOpen": "True when the option list is visible."
      },
      "actions": {
        "setSelection": "Replace the current selection with the given list of values.",
        "clearSelection": "Clear the current selection.",
        "applyFilter": "Set the filter query for the option list.",
        "open": "Open the option list.",
        "close": "Close the option list."
      }
    },
    "muscleScheme": {
      "state": {
        "chart": "Current muscle chart — findings keyed by region id. Notes are PHI and omitted from this snapshot; read them per-region via get_region.",
        "activeView": "Id of the currently displayed muscle view."
      },
      "actions": {
        "focusRegion": "Move focus to a specific region in the active view.",
        "getRegion": "Read the findings (incl. notes) recorded on one region.",
        "regionsWith": "List the region ids carrying a given finding type.",
        "setFinding": "Record a finding (observation / lesion / pain / treatment / note) on a region.",
        "removeFinding": "Remove a single finding type from a region.",
        "clearRegion": "Clear every finding from a region.",
        "setView": "Switch the active muscle view."
      }
    },
    "navGrid": {
      "state": {
        "itemCount": "Number of navigable tiles currently rendered in the grid."
      },
      "actions": {
        "openItem": "Activate the tile with the given id (navigates or fires its select handler)."
      }
    },
    "navigationMenu": {
      "state": {
        "activeItem": "Id of the currently-active top-level navigation item, or null when none is active.",
        "isOpen": "Whether a dropdown / mega-menu panel is currently open."
      },
      "actions": {
        "selectItem": "Activate (open) the top-level item with the given id.",
        "open": "Open the dropdown / mega-menu panel for the given top-level item id.",
        "close": "Close any open dropdown / mega-menu panel."
      }
    },
    "noteTimeline": {
      "state": {
        "notes": "Structural snapshot of the rendered rows: ids and flags only, never note content or author.",
        "visibleCount": "How many notes are rendered, after the maxItems cap is applied."
      }
    },
    "notesPanel": {
      "state": {
        "notes": "Timeline note ids + pinned / flagged / clinical / deleted flags. Never the note body or author (both PHI). A note whose delete is in flight is left out; a pin or flag in flight is reported with its new value. A deleted note takes no pin or flag.",
        "canAddClinical": "Whether the current user may write clinical notes (the practice_add_clinical_note permission gate).",
        "currentNoteId": "Id of the note the host has addressed — the one marked, scrolled to and focused. Null when none is. An id only, never note content."
      },
      "actions": {
        "addNote": "Compose and commit a new note. A clinical note requires the clinical-note permission (read canAddClinical first).",
        "togglePin": "Toggle the pinned state of the note with the given id. Returns the host's promise; a refusal restores the pin and the panel shows the host's message. Rejects without reaching the host when the panel refuses: a deleted note, actions.pin false, or a write for the note still pending.",
        "toggleFlag": "Toggle the flagged state of the note with the given id. Returns the host's promise; a refusal restores the flag and the panel shows the host's message. Rejects without reaching the host when the panel refuses: a deleted note, or a write for the note still pending."
      }
    },
    "noticeGroup": {
      "state": {
        "noteCount": "How many folding notes (not errors or progress notices) are still shown, dismissed ones excluded.",
        "grouped": "True while the notes sit behind one summary row (two or more notes).",
        "open": "Whether the summary row is open (expanded), so every note is visible."
      },
      "actions": {
        "setOpen": "Open (expand) or fold the notes behind the summary row. With a controlled open the host decides."
      }
    },
    "notificationTray": {
      "state": {
        "items": "Currently-displayed notifications. Ids + read flag only — no PHI.",
        "unreadCount": "Badge unread count surfaced by the host.",
        "isOpen": "Whether the dropdown panel is currently open."
      },
      "actions": {
        "open": "Open the notification panel.",
        "close": "Close the notification panel.",
        "selectItem": "Activate the notification with the given id.",
        "markRead": "Request the host to mark a notification as read.",
        "dismiss": "Request the host to dismiss the notification with the given id.",
        "markAllRead": "Request the host to mark every notification as read.",
        "viewAll": "Navigate to the full notification list."
      }
    },
    "numberInput": {
      "state": {
        "value": "Current numeric value, or null when empty.",
        "isEmpty": "Whether the input has no value."
      },
      "actions": {
        "setValue": "Replace the numeric value, or pass null to clear.",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the input."
      }
    },
    "operatorHero": {
      "state": {
        "name": "Operator display name currently rendered.",
        "hasColor": "Whether a calendar-colour identity dot is rendered. Structural flag only — never the colour id or any PHI."
      }
    },
    "opticalPrescription": {
      "state": {
        "prescription": "Current prescription — refraction rows per band/eye plus PD, lens type and axis system. The free-text note is PHI and omitted; read it via get_notes.",
        "bands": "Distance bands carrying at least one refraction value (far / intermediate / near)."
      },
      "actions": {
        "focusEye": "Move focus to an eye's row (first editable cell in interactive mode), optionally within a specific band.",
        "getRow": "Read one eye's refraction row at a distance band.",
        "getNotes": "Read the free-text clinical note on the prescription.",
        "setCell": "Set (or clear, with null) a single refraction cell — sphere / cylinder / axis / add / prism / base.",
        "setValue": "Replace the entire prescription value.",
        "clearBand": "Clear every refraction row in a distance band."
      }
    },
    "otpInput": {
      "state": {
        "value": "Current digits entered, packed into a single string.",
        "isComplete": "Whether all required digits have been entered."
      },
      "actions": {
        "setValue": "Replace the entered digits.",
        "clear": "Empty all digit cells. Loses any typed value.",
        "focus": "Move keyboard focus to the active digit cell."
      }
    },
    "pagination": {
      "state": {
        "currentPage": "Current page index (1-based).",
        "totalPages": "Total number of pages."
      },
      "actions": {
        "goToPage": "Navigate to a specific 1-indexed page.",
        "next": "Advance to the next page.",
        "previous": "Step back to the previous page."
      }
    },
    "passwordInput": {
      "state": {
        "value": "Current password value. Never log or persist this off-device.",
        "isEmpty": "Whether the input has no value.",
        "isRevealed": "Whether the password is currently shown in plain text."
      },
      "actions": {
        "setValue": "Replace the password value.",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the input.",
        "toggleVisibility": "Toggle between masked and plain-text display."
      }
    },
    "patientDetails": {
      "state": {
        "visibleFieldOrder": "Ordered list of visible field KEYS (e.g. \"fiscalCode\"). Never a field value.",
        "patientId": "Opaque patient id the panel is bound to. No patient-identifiable data."
      },
      "actions": {
        "setVisibleFields": "Set which fields are visible (and their order). Unknown ids are ignored.",
        "setFieldOrder": "Reorder the visible fields. Order within a section is honoured; sections stay fixed.",
        "updateField": "Write a patient field value via the consumer's audited save path. Subject to the same permissions and validation as a human edit."
      }
    },
    "patientSearch": {
      "state": {
        "variant": "Active variant — one of `who-where`, `who-where-when`, `faceted`, `discovery`."
      },
      "actions": {
        "reset": "Clear the current search query (WHO, WHERE, WHEN, facets, geo) via the consumer-owned onChange.",
        "focusPrimary": "Move keyboard focus to the most-relevant search field for the current variant."
      }
    },
    "patientSummaryCard": {
      "state": {
        "summary": "Structural summary of the patient card — identifiers, counts, and consent flags only. Never patient identity or clinical content."
      }
    },
    "paymentCard": {
      "state": {
        "number": "Payment number / reference currently rendered.",
        "state": "Resolved headline financial state. Structural state only — never patient identity."
      }
    },
    "paymentRequestCard": {
      "state": {
        "status": "Current lifecycle status of the payment request. Structural state only."
      }
    },
    "paymentForm": {
      "state": {
        "status": "Lifecycle of the current payment attempt. `succeeded` is only reached once Stripe confirmed the PaymentIntent; `failed` covers validation, decline and processor errors alike. Still `idle` after `submit` means the attempt never fired — Stripe had not mounted.",
        "complete": "True when Stripe reports the payment fields, and any required billing address, as complete. Read this before `submit` — outside preview mode the submit control is disabled while it is false, so the call will not be accepted.",
        "errorKey": "i18n key of the current error (e.g. `payment.error.cardDeclined`), or null. The key is the structural datum — the translated message is free text and is never exposed.",
        "paymentIntentId": "Opaque Stripe PaymentIntent id once a charge succeeded, else null. The receipt — the attribution handle for the money that moved. Never the client secret."
      },
      "actions": {
        "submit": "Submit the payment. Charges the configured payment method — irreversible. Resolves once the attempt has settled; the outcome is never in the resolved value, so read `status`, then `errorKey` on failure or `paymentIntentId` on success.",
        "reset": "Clear recoverable errors and return their status to \"idle\". Stripe fields remain unchanged. Pending or paid attempts cannot be reset and require a new session."
      }
    },
    "pdfViewer": {
      "state": {
        "status": "Whether the document has loaded. Read this first — every action is a silent no-op while the viewer is loading or has failed.",
        "currentPage": "1-indexed page currently in view. The read-back for `go_to_page`, which clamps out-of-range requests silently.",
        "pageCount": "Number of pages in the loaded document, or 0 before it loads. Range-check against this before calling `go_to_page` rather than discovering the clamp after the fact.",
        "zoom": "Zoom preset or numeric scale factor currently applied. The read-back for `set_zoom`.",
        "searchMatchCount": "Number of matches for the active search query — 0 when nothing matched or no search is running. A count only; the matched text is never exposed."
      },
      "actions": {
        "goToPage": "Navigate to a specific 1-indexed page. Check `pageCount` first — an out-of-range page is clamped silently, and the call is a no-op until `status` is \"ready\".",
        "setZoom": "Apply a zoom preset or numeric scale factor.",
        "search": "Search the document for the given query and highlight matches. Read the `searchMatchCount` slot afterwards — the action itself returns nothing.",
        "print": "Open the browser print dialog for the document."
      }
    },
    "periodontalChartCard": {
      "state": {
        "toothIds": "FDI ids of the teeth currently charted. Tooth identifiers only — no patient identity.",
        "siteCount": "Total number of probing sites rendered.",
        "deepPocketCount": "Number of sites with a probing depth at or above the 6mm severe threshold."
      }
    },
    "periodontalChart": {
      "state": {
        "indices": "Derived whole-mouth indices: mean probing depth, mean attachment level, plaque %, bleeding %, scored-site and deep-pocket counts. Aggregates only — no per-site or patient data.",
        "findings": "Narratable per-tooth problem list — one entry per flagged tooth (missing, implant, a pocket at/above the moderate threshold, bleeding, suppuration, mobility ≥ 1, or furcation), in clinical order with each tooth’s pockets deepest-first. Each entry carries fdi, status, severity, maxPocketDepthMm, pockets, bleeding/plaque/suppuration sites, maxRecessionMm, mobility and furcation. Read this to summarise the chart aloud. Structural readings + FDI ids only — never the free-text note or patient identity.",
        "missingTeeth": "FDI ids of every tooth flagged as missing.",
        "chartedTeeth": "FDI ids of teeth carrying at least one probing reading or flag. Identifiers only — never patient identity or the free-text note."
      },
      "actions": {
        "focusSite": "Move focus to one per-site input: a probing measurement (pocket depth, recession) or the bleeding or plaque checkbox.",
        "getTooth": "Read one tooth’s structural readings (sites, flags, mobility, furcation). The free-text note is never returned.",
        "getIndices": "Read the derived whole-mouth summary indices.",
        "getFindings": "Read the narratable per-tooth problem list (flagged teeth only). Use it to summarise the chart aloud — e.g. “46: 8 mm disto-buccal pocket, bleeding, grade 2 mobility”. Structural readings and FDI ids only; the free-text note is never returned.",
        "setDepth": "Record the probing pocket depth (mm) at a site.",
        "setRecession": "Record the gingival recession (mm) at a site.",
        "toggleBleeding": "Set bleeding on probing at a site.",
        "togglePlaque": "Set plaque presence at a site.",
        "setMobility": "Set a tooth’s Miller mobility grade (0–3).",
        "setMissing": "Flag a tooth present or absent.",
        "clearTooth": "Clear every reading from a tooth."
      }
    },
    "periodontalDiagnosis": {
      "state": {
        "diagnosis": "Recorded stage (1–4), grade (A–C), extent, and ticked contributing-factor ids. The free-text note is never included.",
        "summary": "The composed, localised diagnosis sentence (e.g. \"Periodontitis — Stage III · Grade B · generalised\").",
        "complete": "True once both a stage and a grade are recorded."
      },
      "actions": {
        "getDiagnosis": "Read the recorded stage / grade / extent / factors. The note is never returned.",
        "setStage": "Record the periodontitis stage (I–IV).",
        "setGrade": "Record the periodontitis grade (A–C).",
        "setExtent": "Record the distribution / extent.",
        "toggleFactor": "Tick or clear a contributing factor.",
        "clearDiagnosis": "Reset the whole diagnosis to empty."
      }
    },
    "periodontalRiskChart": {
      "state": {
        "overall": "Overall periodontal risk verdict — \"low\" | \"moderate\" | \"high\". Aggregate only.",
        "factorLevels": "Per-factor risk levels keyed by factor (bleeding, pockets, toothLoss, boneLossAge, smoking, systemic) — each \"low\" | \"moderate\" | \"high\". No raw values."
      }
    },
    "permissionMatrix": {
      "state": {
        "selection": "Ids of currently-granted editable permissions. Locked owner permissions are excluded."
      },
      "actions": {
        "setSelection": "Replace the granted permissions with the given ids. Locked and unknown ids are ignored.",
        "clear": "Revoke every editable permission. Locked owner permissions are unaffected."
      }
    },
    "phoneInput": {
      "state": {
        "value": "Current E.164 phone number, or empty string when not yet valid.",
        "isEmpty": "Whether the input has no value."
      },
      "actions": {
        "setValue": "Replace the phone number (E.164 or national format; parser will infer country).",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the phone-number input."
      }
    },
    "popover": {
      "state": {
        "isOpen": "True when the popover is currently open."
      },
      "actions": {
        "open": "Open the popover.",
        "close": "Close the popover."
      }
    },
    "positionGrid": {
      "state": {
        "value": "Currently selected position value (e.g. \"top-start\"), or empty string when none."
      },
      "actions": {
        "setValue": "Select the cell with the given position value."
      }
    },
    "postalCodeInput": {
      "state": {
        "value": "Current postal code value.",
        "isEmpty": "Whether the field has no value."
      },
      "actions": {
        "setValue": "Replace the postal code value.",
        "clear": "Empty the field. Loses any typed value."
      }
    },
    "practiceProfileHero": {
      "state": {
        "name": "Practice display name currently rendered.",
        "hasCover": "Whether a cover-image strip is rendered. Structural flag only — never the image URL or any PHI."
      }
    },
    "practiceResults": {
      "state": {
        "variant": "Active variant — one of `split-list-map`, `map-first`, `rich-cards`."
      },
      "actions": {
        "scrollToResult": "Scroll the result card with the given id into the visible viewport.",
        "focusPin": "Highlight the map pin for the given result id (no-op on rich-cards)."
      }
    },
    "pregnancyDating": {
      "state": {
        "inputs": "The dating inputs as entered — anchor method, its reference date, cycle length (LMP), embryo age (IVF), the optional CRL + scan date, the manual ±days nudge, and the evaluation date. Dates are `yyyy-MM-dd`. Read this to see which method a reference date will be interpreted under.",
        "result": "Computed estimated delivery date, the GA day-0 anchor after any re-dating or adjustment, the gestational age in completed weeks + days as of the evaluation date, and the trimester band (preconception / first / second / third / postterm). Null until a reference date is set."
      },
      "actions": {
        "setMethod": "Choose which anchor the pregnancy is dated from. The reference date is kept and re-interpreted under the new method, so set the method first — only a known EDD may be in the future.",
        "setReferenceDate": "Set the reference date for the active method. This is the only required input — the result stays null without it.",
        "setCycleLength": "LMP method only — set the average cycle length, which shifts Naegele dating off the 28-day assumption.",
        "setEmbryoAge": "IVF method only — set the embryo age at transfer, which offsets the transfer date back to a notional LMP.",
        "setCrl": "LMP method only — set the first-trimester crown-rump length and its scan date. A confirmed re-dating wins over both the cycle length and the manual adjustment.",
        "setDatingAdjustment": "Nudge the dating by whole days for an uncertain anchor. Positive moves the pregnancy further along (earlier effective LMP, earlier due date).",
        "setAsOfDate": "Set the date gestational age, trimester and the antenatal schedule are evaluated against. Defaults to today; the estimated delivery date itself does not depend on it."
      }
    },
    "pregnancyWeightGain": {
      "state": {
        "inputs": "The four entered measurements — height (cm), pre-pregnancy weight (kg), current weight (kg) and completed gestational weeks. Null for anything not yet entered.",
        "plurality": "Number of babies the assessment is keyed to. Drives which IOM band applies — twins use the higher twin bands, triplets have no published band.",
        "result": "Computed assessment — actual gain (kg), the IOM recommended-to-date and total bands, and where the gain sits against them (below / within / above). Null until every input is valid; the bands are null when the IOM publishes none (triplets, underweight twins)."
      },
      "actions": {
        "setValue": "Set one or more of the four measurements. Omitted fields are left unchanged; the assessment recomputes as soon as all four are valid.",
        "setPlurality": "Choose the number of babies. Twins use the higher IOM twin bands; triplets have no published IOM range, so only the actual gain is reported.",
        "reset": "Clear the four measurements and the computed assessment. The plurality choice is a setting and is kept.",
        "getResult": "Read back the computed assessment — the same value the `result` state slot returns. Null until every input is valid."
      }
    },
    "privacyLock": {
      "state": {
        "isLocked": "True when the privacy overlay is open."
      },
      "actions": {
        "lock": "Open the privacy overlay.",
        "unlock": "Close the privacy overlay (consumer-confirmed action)."
      }
    },
    "productHubPanel": {
      "state": {
        "items": "Currently-displayed releases. Ids + unseen flag only — no content.",
        "unseenCount": "Number of displayed releases still marked unseen."
      },
      "actions": {
        "selectItem": "Activate the release with the given id (fires the host analytics hook)."
      }
    },
    "productHubTray": {
      "state": {
        "unseenCount": "Number of unseen product releases — the trigger badge count."
      },
      "actions": {
        "open": "Open the product-updates panel.",
        "close": "Close the product-updates panel."
      }
    },
    "progressNotice": {
      "state": {
        "value": "Progress so far, in the units of max.",
        "max": "The value at which the import is done.",
        "complete": "True once every item is ready or the value has reached max.",
        "readyCount": "How many items in the details list are ready.",
        "itemCount": "How many items the details list has (0: no list).",
        "detailsOpen": "Whether the details list is open."
      },
      "actions": {
        "setDetailsOpen": "Open or close the details list of the import."
      }
    },
    "progress": {
      "state": {
        "value": "The figure the bar currently shows, in the same units as the `max` prop (default max 100) — including a value written imperatively by `set_value`. Null while indeterminate.",
        "indeterminate": "True when the operation reports no measurable progress. Read this before treating a null/0 `value` as \"nothing done yet\" — the two are different states."
      },
      "actions": {
        "setValue": "Paint the displayed progress value straight onto the DOM, bypassing React. Read it back with the `value` state slot — the `value` prop does not change."
      }
    },
    "provinceSelect": {
      "state": {
        "value": "Selected province code (UPPERCASED sigla for Italy), or null when nothing is selected."
      },
      "actions": {
        "setValue": "Set the selected province by code.",
        "clearSelection": "Clear the selected province."
      }
    },
    "publicFooter": {
      "state": {
        "variant": "Active variant — one of `minimal`, `compact`, `full`.",
        "legalUrls": "Resolved legal URLs for the active locale + marketingBaseUrl + overrides: `{ privacy, terms, contacts }`."
      }
    },
    "publicHeader": {
      "state": {
        "menuOpen": "Whether the mobile drawer is currently open."
      },
      "actions": {
        "openMenu": "Open the mobile drawer.",
        "closeMenu": "Close the mobile drawer."
      }
    },
    "qrCode": {
      "state": {
        "value": "The string (typically an https URL) encoded into the QR."
      }
    },
    "radioGroup": {
      "state": {
        "value": "Currently selected radio value, or empty string when none."
      },
      "actions": {
        "setValue": "Select the radio with the given value. A value that matches no enabled radio is ignored."
      }
    },
    "radiographPanel": {
      "state": {
        "radiographs": "Structural references (id + kind) to the films shown for the selected tooth."
      },
      "actions": {
        "viewRadiograph": "Open a film in the fullscreen viewer by id.",
        "closeViewer": "Close the fullscreen radiograph viewer."
      }
    },
    "recaptchaWidget": {
      "state": {
        "hasToken": "True when the widget currently holds a valid verification token."
      },
      "actions": {
        "execute": "Programmatically trigger the captcha challenge. Issues a token via the callback.",
        "reset": "Clear the current token and re-roll the captcha. Irreversible."
      }
    },
    "resizable": {
      "state": {
        "sizes": "Current per-panel sizes as percentages of the group."
      },
      "actions": {
        "setSizes": "Replace the panel sizes (percentages, summing to 100)."
      }
    },
    "reviewsPanel": {
      "state": {
        "filter": "Active star-bucket filter — `\"all\"` or an integer 1..5. Reads the controlled `filter` prop.",
        "visibleCount": "Number of reviews currently rendered after the filter is applied."
      },
      "actions": {
        "setFilter": "Narrow the visible review list to a star bucket, or `\"all\"` to clear the filter."
      }
    },
    "richTextEditor": {
      "state": {
        "html": "Sanitised HTML representation of the editor content.",
        "text": "Plain-text content of the editor (tags stripped).",
        "markdown": "Markdown representation of the editor content. The Redactor engine has no native Markdown serialiser, so this is always empty — read `html` instead.",
        "aiBusy": "True while a one-shot AI action (a practice template, spelling or auto-format) is rewriting the note. Read `html` once it is false."
      },
      "actions": {
        "setContent": "Replace the editor content. Loses unsaved input.",
        "insertAtCursor": "Insert content at the caret (replacing any selection). HTML is sanitised; pass asHtml:false for plain text. Seam for merge/template variables and AI-generated drafts.",
        "applyFormat": "Toggle/apply a block or inline format on the current selection (bold, italic, strikethrough, bulleted/numbered list, heading, quote, paragraph).",
        "clear": "Empty the editor. Loses unsaved input.",
        "focus": "Move keyboard focus to the editor surface."
      }
    },
    "searchBar": {
      "state": {
        "query": "Current raw query string (inline mode). Empty in launcher mode.",
        "isEmpty": "True when the query is empty."
      },
      "actions": {
        "setQuery": "Replace the current query (inline mode only). No-op in launcher mode.",
        "clear": "Clear the query and close the result popover.",
        "submit": "In launcher mode opens the CommandPalette; in inline mode commits the highlighted option.",
        "focus": "Move keyboard focus to the SearchBar.",
        "applySavedSearch": "Apply a saved-search by id. The host wires the actual query lookup; the adapter routes the id to its onApplySavedSearch handler. No-op when no handler is wired."
      }
    },
    "searchInput": {
      "state": {
        "query": "Current raw query string (not debounced).",
        "isEmpty": "True when the query is empty."
      },
      "actions": {
        "setQuery": "Replace the current query with the given string and fire onChange immediately.",
        "clear": "Clear the query and return focus to the input.",
        "submit": "Fire the current query without waiting for debounce.",
        "focus": "Move keyboard focus to the input (expanding it first if `variant=\"expandable\"`)."
      }
    },
    "select": {
      "state": {
        "value": "Currently selected option value, or empty string when none.",
        "isOpen": "Whether the option list is open."
      },
      "actions": {
        "setValue": "Select the option with the given value. A value that is not among the options, belongs to a disabled option, or is an action option (such as New signatory), is ignored and the selection stays as it was.",
        "clear": "Clear the current selection.",
        "open": "Open the option list.",
        "close": "Close the option list."
      }
    },
    "sheet": {
      "state": {
        "isOpen": "True when the sheet is currently open."
      },
      "actions": {
        "open": "Open the sheet.",
        "close": "Close the sheet. Reversible by reopening.",
        "dismiss": "Dismiss the sheet. Loses any in-progress input that has not been committed."
      }
    },
    "sidebar": {
      "state": {
        "isCollapsed": "True when the sidebar is in its collapsed (rail) state.",
        "anySectionOpen": "True while any foldable section is open."
      },
      "actions": {
        "toggleCollapsed": "Toggle the sidebar between collapsed and expanded.",
        "setAllSectionsOpen": "Fold (false) or unfold (true) every foldable section, as the sections toggle does."
      }
    },
    "signatureCapture": {
      "state": {
        "isEmpty": "True when no signature has been drawn or typed."
      },
      "actions": {
        "clear": "Erase the current signature. Loses unsaved input.",
        "undo": "Undo the last stroke.",
        "confirm": "Commit the signature and return its serialised payload."
      }
    },
    "skeletonScheme": {
      "state": {
        "chart": "Current skeletal chart — findings keyed by bone region id. Notes are PHI and omitted from this snapshot; read them per-region via get_region.",
        "activeView": "Id of the currently displayed skeletal view."
      },
      "actions": {
        "focusRegion": "Move focus to a specific bone in the active view.",
        "getRegion": "Read the findings (incl. notes) recorded on one bone.",
        "regionsWith": "List the bone region ids carrying a given finding type.",
        "setFinding": "Record a finding (observation / lesion / pain / treatment / note) on a bone.",
        "removeFinding": "Remove a single finding type from a bone.",
        "clearRegion": "Clear every finding from a bone.",
        "setView": "Switch the active skeletal view."
      }
    },
    "slider": {
      "state": {
        "value": "Current thumb values; length 1 for a single slider, length 2 for a range."
      },
      "actions": {
        "setValue": "Replace the thumb value(s). Pass a 1-tuple for single, 2-tuple for range."
      }
    },
    "slotGrid": {
      "state": {
        "status": "Visual state of the grid. Read this before concluding \"no availability\" — `loading` means the window has not resolved yet.",
        "selectedSlotId": "Id of the slot the consumer has committed as selected, or null. Reflects the controlled `selectedSlotId` prop, not an internal choice.",
        "visibleDays": "Calendar days currently rendered as columns, as `yyyy-MM-dd` keys in display order.",
        "slotsByDay": "Map of day key (`yyyy-MM-dd`) to the ids of that day’s bookable slots. Ids only — never slot or patient objects. Includes slots still collapsed behind \"Show more\".",
        "expandedDays": "Day keys whose \"Show more\" control has been used, so every slot for that day is rendered.",
        "hasPrevious": "True when an earlier window of days exists — the \"previous days\" arrow is offered.",
        "hasNext": "True when a later window of days exists — the \"next days\" arrow is offered."
      },
      "actions": {
        "selectSlot": "Select an available slot by id. Emits the same selection the user would by clicking the slot button.",
        "nextDays": "Page the visible window forward to the next group of days, as the \"next days\" arrow does.",
        "previousDays": "Page the visible window back to the previous group of days, as the \"previous days\" arrow does.",
        "showMoreDay": "Render every slot for one day, as the day’s \"Show more\" button does. Only affects what is on screen — `slotsByDay` already lists collapsed slots."
      }
    },
    "sparkline": {
      "state": {
        "annotationIds": "Ids of the annotations currently drawn, in declaration order. The only ids `select_annotation` will accept — check against this before calling. Ids only, never annotation labels.",
        "selectable": "True when the host wired an annotation-select handler. False means every `select_annotation` call is a no-op, whatever id is passed.",
        "pointCount": "Number of positions in the currently rendered series, unmeasured gaps included — reflecting whichever applied last, the `data` prop or an `update_series` call. A count only; the plotted values are never exposed."
      },
      "actions": {
        "updateSeries": "Replace the sparkline data without remounting. Writes straight into ApexCharts, bypassing React — read the `pointCount` slot back rather than expecting the host tree to reflect it. Host-supplied point labels are not updated, so the tooltip title is hidden after this call.",
        "exportPng": "Return a PNG data URI of the sparkline.",
        "selectAnnotation": "Select a drawn annotation, as pressing its band would. No-op unless the host wired onAnnotationSelect and the id names an annotation that is currently drawn — check the `selectable` and `annotationIds` slots first, since neither failure produces a signal."
      }
    },
    "stepperAccordion": {
      "state": {
        "currentStep": "Zero-indexed active step.",
        "totalSteps": "Total number of steps in the flow.",
        "openSteps": "Indices of currently-open step panels (singleton under Radix single mode)."
      },
      "actions": {
        "gotoStep": "Activate the given step. Upcoming steps are gated by the consumer’s validation in onStepChange.",
        "next": "Advance to the next step. Subject to the consumer’s onStepChange validation.",
        "previous": "Return to the previous step (re-opens a completed step in edit mode)."
      }
    },
    "stepperProgress": {
      "state": {
        "currentStep": "Zero-indexed active step.",
        "totalSteps": "Total number of steps in the flow."
      },
      "actions": {
        "gotoStep": "Request the host to activate the given step via onStepChange. No-op when no handler is wired.",
        "next": "Request the host to advance one step via onStepChange.",
        "previous": "Request the host to retreat one step via onStepChange."
      }
    },
    "suggestionChip": {
      "state": {
        "value": "The chip's emitted value (its `value` prop or string child)."
      },
      "actions": {
        "select": "Activate the chip — fires onSelect and (by default) self-removes.",
        "dismiss": "Remove the chip without activating it."
      }
    },
    "switch": {
      "state": {
        "checked": "Whether the switch is on."
      },
      "actions": {
        "setChecked": "Set the on/off state to a specific value.",
        "toggle": "Flip the on/off state."
      }
    },
    "tabs": {
      "state": {
        "activeTab": "Value of the currently-active tab."
      },
      "actions": {
        "selectTab": "Activate the tab with the given value."
      }
    },
    "taskEditor": {
      "state": {
        "status": "Where the form is: loading the task, ready, failed to load, locked (edit mode with nothing to load) or withheld behind the privacy notice. Writes need 'ready'.",
        "draft": "The draft as Save would send it: ids and structure only, never the note body. done is the last value the host confirmed.",
        "canSave": "Whether save would reach the host now: the form is ready, the note is filled, a chosen counterpart has an id and no write is pending."
      },
      "actions": {
        "setFields": "Set draft fields. Ids must come from the host lists, a patient the host search returned, or equal the stored one. At most one counterpart per call, and it selects its branch; assigneeId null is nobody. Nothing is saved.",
        "save": "Save the task through the host. Resolves once the write lands and rejects with the host error, which the form also shows.",
        "setDone": "Mark an existing task done or not done through the host. A rejection rolls the switch back.",
        "delete": "Delete the task through the host, without the confirmation dialog. Resolves once the delete lands.",
        "cancel": "Close the editor without saving. Anything typed since the last save is lost. Refused while a save or delete is in flight."
      }
    },
    "taskTray": {
      "state": {
        "items": "Currently-displayed tasks. Ids + status only — never task title / patient."
      },
      "actions": {
        "selectItem": "Activate the task with the given id.",
        "toggleDone": "Request the host to toggle the done state of a task.",
        "dismiss": "Request the host to dismiss / remove the task with the given id."
      }
    },
    "tasksPanel": {
      "state": {
        "items": "Currently-rendered tasks. Ids + status only — never task note / patient."
      },
      "actions": {
        "selectItem": "Activate the task with the given id.",
        "toggleDone": "Request the host to toggle the done state of a task.",
        "dismiss": "Request the host to dismiss / remove the task with the given id.",
        "jumpToOverdue": "Re-anchor the panel scroll on the overdue tasks.",
        "setFilter": "Set the task filters (status segment, type, assignee, overdue-only). Omitted keys are left unchanged. Unchanged values are not re-sent. In server-filtered panels, choosing status 'done' clears overdueOnly.",
        "changeScope": "Switch the task scope segment between this patient ('patient') and the operator's own tasks ('mine'). The host re-feeds the items.",
        "loadMore": "Request the host to append the next page of tasks."
      }
    },
    "textArea": {
      "state": {
        "value": "Current value of the textarea.",
        "isEmpty": "Whether the textarea has no value."
      },
      "actions": {
        "setValue": "Replace the textarea value.",
        "clear": "Empty the textarea. Loses any typed value.",
        "focus": "Move keyboard focus to the textarea."
      }
    },
    "textInput": {
      "state": {
        "value": "Current value of the text input.",
        "isEmpty": "Whether the input has no value."
      },
      "actions": {
        "setValue": "Replace the input value.",
        "clear": "Empty the input. Loses any typed value.",
        "focus": "Move keyboard focus to the input."
      }
    },
    "themeToggle": {
      "state": {
        "currentTheme": "Active theme preference: light, dark, or system.",
        "currentAccessibility": "Active accessibility preference: default or accessible."
      },
      "actions": {
        "setTheme": "Switch the theme preference.",
        "setAccessibility": "Switch the accessibility preference."
      }
    },
    "timePicker": {
      "state": {
        "value": "Currently selected time as 24h HH:MM. Null when unset with allowEmpty; otherwise defaults to 00:00.",
        "isOpen": "Whether the time dropdown is open. Always false unless the picker has the dropdown prop."
      },
      "actions": {
        "setTime": "Set the time using a 24h HH:MM string.",
        "clear": "Clear to null with allowEmpty; otherwise reset to 00:00.",
        "open": "Open the time dropdown without moving focus. A no-op unless the picker has the dropdown prop and is enabled.",
        "close": "Close the time dropdown. A no-op unless the picker has the dropdown prop."
      }
    },
    "timeline": {
      "state": {
        "selectedEvent": "Opaque event id of the currently-active event, or null."
      },
      "actions": {
        "selectEvent": "Scroll the given event into view and mark it as selected."
      }
    },
    "toast": {
      "state": {
        "visibleToasts": "Snapshot of currently-visible toasts by opaque id. Never carries the toast body text."
      },
      "actions": {
        "dismiss": "Dismiss the toast with the given id. Irreversible from the same UI.",
        "dismissAll": "Dismiss every visible toast."
      }
    },
    "tokenInsertMenu": {
      "state": {
        "isOpen": "True when the token menu is currently open."
      },
      "actions": {
        "open": "Open the token menu.",
        "close": "Close the token menu.",
        "insert": "Emit a merge-token to the consumer for cursor insertion."
      }
    },
    "toolRail": {
      "state": {
        "panelMode": "How an open panel shows: docked beside the content, over it because the frame keeps a minimum width for main, or full screen on a phone."
      }
    },
    "toothFindingPalette": {
      "state": {
        "activeFinding": "Id of the armed finding tool, or null when no tool is selected. Identifies a finding TYPE — never a patient, and never a charted finding.",
        "status": "Execution status the next charted finding takes — 'planned' or 'executed'.",
        "tools": "Every finding id the palette currently offers, in render order — built-ins plus any practice-defined ids. The authoritative vocabulary for select_finding."
      },
      "actions": {
        "selectFinding": "Arm a finding tool, so the next tooth (or surface, or root) picked on the paired dental chart records that finding.",
        "clearSelection": "Disarm the palette so clicks on the chart stop recording a finding. Charted findings are untouched.",
        "setStatus": "Choose whether the next charted finding is recorded as planned or executed.",
        "focusTool": "Move keyboard focus to a tool without arming it — relocates the toolbar's single roving tab stop."
      }
    },
    "toothScheme": {
      "state": {
        "chart": "Current dental chart keyed by FDI id, or by a special-element id (quadrant / sextant / arch / general) for region-spanning findings. Each entry carries the flat `conditions` + `surfaces` pair AND the normalised per-symbol list, where every finding scopes its own surfaces, roots, planned/executed status and colour. Free-text clinical notes are PHI and omitted — read them per tooth via get_symbols / get_notes.",
        "chartedTeeth": "Ids carrying at least one finding — FDI tooth ids and/or special-element ids (quadrant / sextant / arch / general).",
        "radiographs": "Structural references (id + kind) to the radiographs on each tooth, keyed by FDI id. Notes, dates and image URLs are not exposed here — fetch them on demand via get_radiographs.",
        "teethWithRadiographs": "FDI ids of every tooth carrying at least one radiograph."
      },
      "actions": {
        "focusTooth": "Move focus to a specific tooth by FDI id.",
        "getTooth": "Read the BUILT-IN findings recorded on one tooth (FDI id), as a flat list of condition ids. Practice-defined findings, per-finding surfaces, roots and status are not included — use get_symbols for the tooth's full record.",
        "getSymbols": "Read one tooth's full finding record (incl. notes) — one entry per symbol, each with its own surfaces, roots, planned/executed status, colour override, provenance id and free-text note. Normalised, so a tooth charted with the flat conditions/surfaces pair reports the same shape.",
        "getNotes": "Read the tooth-level free-text clinical note, or null when there is none. Per-finding notes are separate — they come back from get_symbols.",
        "teethWith": "List the ids carrying a given built-in finding — FDI tooth ids and/or special-element ids (e.g. all teeth with caries). For a practice-defined finding use teeth_with_symbol.",
        "teethWithSymbol": "As teeth_with, but open to the practice-defined ids in the host's `conditionRegistry` as well as the built-ins.",
        "getRadiographs": "Read the radiographs (x-rays) attached to one tooth, including their notes, dates and image urls.",
        "openRadiographs": "Focus a tooth and open its radiograph archive (fires the host's onOpenRadiographs).",
        "setFinding": "Record a built-in finding on a tooth or region element (crown, implant, root canal, caries, …). Optionally mark the affected surfaces for a surface finding. Structural only — to set a finding’s roots, planned/executed status, colour or note, or to chart a practice-defined finding, use set_symbol.",
        "setSymbol": "Add or replace one finding on a tooth or region element, with its own part, surfaces, roots, planned/executed status, colour override and note. Matched by `symbol.id` when the symbol carries provenance, otherwise by its condition; appended when nothing matches. Accepts practice-defined `conditionRegistry` ids as well as the built-ins.",
        "removeFinding": "Remove a finding from a tooth — every symbol carrying that condition goes, whatever its surfaces or roots. Built-in ids only.",
        "clearTooth": "Clear every finding from a tooth."
      }
    },
    "transcriptPanel": {
      "state": {
        "selectedTurn": "Opaque id (line index) of the currently-active turn, or null when none is active."
      },
      "actions": {
        "focusTurn": "Scroll the given turn into view and focus its seek button.",
        "copyTurn": "Copy the given turn's text to the clipboard."
      }
    },
    "unitConverter": {
      "state": {
        "category": "Active measurement family. Determines which from/to units are selectable.",
        "inputs": "Source value plus the from-unit and to-unit ids within the active category.",
        "result": "Converted value in the to-unit, or null when the source value is empty."
      },
      "actions": {
        "setValue": "Set the source value to convert.",
        "setUnits": "Set the from-unit and/or to-unit. Ids outside the active category are ignored.",
        "setCategory": "Switch the measurement family. Resets the from/to units to that category defaults.",
        "getResult": "Return the converted value in the to-unit, or null when no source value is set."
      }
    },
    "variablesMenu": {
      "state": {
        "query": "Current filter text.",
        "visibleTokens": "Tokens of the variables currently visible, in row order."
      },
      "actions": {
        "selectToken": "Insert the given variable token at the anchor.",
        "close": "Dismiss the menu without inserting anything."
      }
    },
    "voiceControlPanel": {
      "state": {
        "status": "Where the host voice session is: idle, starting, listening, paused or error.",
        "candidateIds": "Ids of the matches currently offered, in spoken order (option 1 first). Empty when there is nothing to choose.",
        "commandsOpen": "True when the voice commands list is expanded.",
        "panelOpen": "Floating layout: true when the panel above the pill is open. Always false inline.",
        "creditsExhausted": "True when the practice is out of AI credits: every start shows the out-of-credits notice instead of listening.",
        "creditsNoticeOpen": "True when the out-of-credits notice is showing in the panel, in place of the controls."
      },
      "actions": {
        "startListening": "Ask the host to start listening (it opens the microphone). Does nothing when already starting or listening, or while the panel is disabled. Without patient AI consent it shows the consent banner instead; out of AI credits (creditsExhausted) it shows the out-of-credits notice instead.",
        "stopListening": "Ask the host to stop listening, cancel a start or end a paused session. Never gated by consent or AI credits. Does nothing when idle or stopped.",
        "pickCandidate": "Pick one of the offered matches by id, the same as saying its option number or pressing it. Unknown ids are ignored.",
        "openCommands": "Expand the voice commands list.",
        "closeCommands": "Collapse the voice commands list.",
        "openPanel": "Floating layout: open the panel above the pill. Does nothing inline.",
        "closePanel": "Floating layout: close the panel above the pill. Listening carries on. Does nothing inline.",
        "closeCreditsNotice": "Close the out-of-credits notice and go back to the controls. Credits stay out: the next start shows it again."
      }
    },
    "widgetPicker": {
      "state": {
        "open": "Whether the picker dialog is currently open.",
        "items": "Widget-type ids on offer and whether each is pickable. Ids only — titles are host-app strings."
      },
      "actions": {
        "pickWidget": "Pick a widget type from the open catalogue. The picker closes and the host adds the widget. Refused while the dialog is closed and for disabled items."
      }
    },
    "workflowStatus": {
      "state": {
        "value": "The current non-sensitive workflow status key.",
        "isOpen": "Whether the permitted workflow status list is open.",
        "isSaving": "Whether the host is saving a workflow status change."
      },
      "actions": {
        "setValue": "Request a permitted workflow status through the host-owned change callback.",
        "open": "Open the permitted workflow status list.",
        "close": "Close the workflow status list."
      }
    },
    "workflowEditor": {
      "state": {
        "totalSteps": "Total number of nodes currently in the workflow graph.",
        "currentStep": "Opaque node id of the currently-focused step, or null."
      },
      "actions": {
        "gotoStep": "Focus a node by its opaque id."
      }
    },
    "aliaAssistant": {
      "workflows": {
        "sendMessage": "Type a question into the composer and send it; Alia streams a reply into the conversation.",
        "pickSuggestion": "Send one of the suggested prompts as the next user message.",
        "stopGenerating": "Cancel the in-flight streaming response while Alia is generating.",
        "retryMessage": "Re-send a message that failed to send. The failed message is located by its `data-status=\"error\"` ChatMessage (addressed via `data-component-id`); activating that message's built-in retry control re-submits it. No adapter action — the retry control is a DOM-click affordance on the errored message.",
        "togglePopout": "Open or close the floating Alia popout panel (FAB + Sheet) over the current page.",
        "scrollToLatest": "Scroll the conversation log to the newest message.",
        "reviewUsage": "Read the assistant usage meter; when the quota is full the composer locks behind an upgrade CTA."
      }
    },
    "anamnesis": {
      "workflows": {
        "startIntake": "Open a fresh pre-visit questionnaire for the patient to begin the intake interview.",
        "navigateSection": "Move between questionnaire sections in the intake, reading the current section index.",
        "saveIntake": "Persist the patient's questionnaire answers. Writes patient-identifiable clinical history.",
        "selectView": "Switch the diagnostic workspace between the read-only summary of the patient's questionnaire, the current-visit compile view and the across-visits evolution view. The summary is selected on arrival.",
        "recordMeasurement": "Record or correct an anthropometric, dietary-intake (daily energy, protein) or blood-test value for the current visit. BMI is computed from weight and height and cannot be recorded. Writes patient clinical data.",
        "setTarget": "Set or clear the patient's target for one parameter. Every parameter the practice tracks as a goal is set from the summary strip at the top of the Diagnosis surface — the kit seeds target weight, target BMI, daily energy and protein, and a practice can track any numeric parameter it flags as a goal; a targetable parameter that is not tracked (waist, by default) is set from its capture block, through the same editor. Each tile's edit button opens a small form: nothing is committed until Save, a value outside the parameter's sane range is refused rather than silently clamped, and Clear removes the goal rather than setting it to zero. A target belongs to the record, not to a visit — setting one records no measurement. Writes patient clinical data.",
        "assignPathologies": "Assign or remove coded pathologies for the current visit via the multi-select, reading the current assignments.",
        "readIntakeDigest": "Read the Summary tab: the at-a-glance digest of the patient's questionnaire — why they came, the conditions and medication they carry, the symptoms they report and their daily habits — and, one disclosure below it, the full answer list with its from-patient origin markers. Read-only; clinician-only; no mutation.",
        "resolveMissingAnswers": "Read how many required questionnaire answers are still missing, jump to the first of them — which crosses from the read-only Summary to the Compile tab and opens the unanswered-questions block that holds it — and record the answer there. Writes patient clinical history.",
        "readVisitNote": "Open the note recorded at one visit from the evolution grid's visit-column header and read the clinical note and the nutritional proposal. Where the host supplies a side panel (`onOpenNote`), the trigger hands that visit's id out and the host's panel navigates to it — no popover opens, and the note lives in exactly one place. Where it does not, the same trigger opens the note in place. Clinician-only; no mutation.",
        "openRecordSection": "Open or fold one section of the compile form. Sections filled at every visit start open — visit details, any unanswered required questionnaire answers, anthropometrics, pathologies, therapy, the proposal; the occasional ones (oncological history, blood tests, fasting cycles, annual programme) start folded and state in their header how many entries they already hold. Clinician-only; view state only.",
        "windowVisits": "Choose which visits the evolution grid shows as columns, by ticking them in the Visits menu — one button that opens a list of checkbox items, with a Select all row first. Every visit is ticked on arrival; un-ticking one removes its column, the menu stays open while ticking, and the last remaining visit cannot be un-ticked. The change between two adjacent shown columns is always drawn. The button itself reads how many visits are shown out of how many exist. Clinician-only; view state only.",
        "annotateTrend": "Drop a dated event's reference line across every trend sparkline in the evolution grid, by pressing its chip in the event bar above the grid. Events are things that happened to the patient on a date — a therapy or diet starting, a fasting-mimicking cycle, surgery, a change in daily life — and need not have happened at a visit or been recorded by this practice. Each chip carries its category (and, for an event this practice did not record, its source) as an accessible description rather than a hover tooltip. Nothing is drawn on arrival; pressing the same chip again removes the line. Where the host supplies a side panel, turning a chip on also navigates that panel to the event; turning it off does not. Clinician-only; view state only.",
        "chooseExportAudience": "Choose who a file or a shared copy is for — the practice, or the patient — from the single Export & share control beside the view switch. The choice governs every route below it (share, PDF, print, Excel, CSV), it sticks across tabs and later exports, and a non-default choice is badged on the closed control. The menu names what the copy carries in BOTH states, and the exclusions are PER VIEW: on Compile a patient copy drops blood-test values, diagnostic codes, oncological history and the clinician's own notes; on Evolution it drops the blood-test rows; on Summary it drops nothing. Nothing is filed by making the choice. View state only.",
        "exportTab": "Produce a file of the view that is currently selected — Summary, Compile or Evolution — from the one Export & share control. The same document is written as a PDF, an Excel workbook or a CSV, and Print sends that same document to the printer rather than the screen, so the app shell never appears on the page. On Evolution the file follows the visit columns the grid is DRAWING unless \"include every visit\" is ticked; the same Evolution file is also offered from the trend tab of questionnaire mode and from the read-only record, at the end of the trend toolbar. The audience rides in the filename, so the two copies stay apart outside the app. Every table resolves through the patient-copy boundary first, so a patient-audience file is missing what that audience may not receive. Produces a patient-identifiable document.",
        "previewShare": "Open the share preview for the selected view, choose the recipient, and read what that recipient's copy would hold and what it would leave out. THIS SENDS NOTHING: the dialog is a preview of the shape of a share. Real delivery — a signed expiring link, the consent gate, and the record of who received what over which channel — is not part of this surface. Confirming only sets the recipient the next export uses. No mutation, no transmission.",
        "reviewRecord": "Review a completed anamnesis in the care plan: expand the intake answers and read the record. No mutation."
      }
    },
    "appointments": {
      "workflows": {
        "filterAppointments": "Narrow the agenda by date range, patient, operator, chair, type, or status using the per-column filters.",
        "groupByOperator": "Toggle the \"group by operator\" view so appointments nest under their booked clinician.",
        "addTreatment": "Add a treatment line to an appointment from its row action menu.",
        "rescheduleAppointment": "Move an open appointment to a new slot via its row \"Reschedule\" action. Closed (done / absent / cancelled) appointments cannot be rescheduled.",
        "cancelAppointment": "Cancel an open appointment via its row \"Cancel appointment\" action. Destructive — frees the chair and drops the booking.",
        "executeTreatment": "Mark a planned treatment line as done from its line action menu.",
        "contactPatient": "Message the patient on WhatsApp from the appointment row (e.g. send a confirmation reminder)."
      }
    },
    "aestheticCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's aesthetic-medicine care plans from the plan list.",
        "addPlan": "Create a new aesthetic-medicine care plan for the patient.",
        "inspectArea": "Focus a body or face region on the anatomy anchor and read its charted treatment areas.",
        "markTreatmentDone": "Mark a planned treatment as executed from its row action in the treatments grid.",
        "addTreatment": "Add a planned treatment row to the current care plan.",
        "removeTreatment": "Remove the selected treatment(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the aesthetic-medicine care plan.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "dermatologyCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's dermatology / skin-lesion care plans from the plan list.",
        "addPlan": "Create a new dermatology / skin-lesion care plan for the patient.",
        "inspectLesion": "Focus a body or face region on the skin-lesion anchor and read its charted lesions.",
        "markTreatmentDone": "Mark a planned procedure as executed from its row action in the treatments grid.",
        "addTreatment": "Add a planned procedure row to the current care plan.",
        "removeTreatment": "Remove the selected procedure(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the dermatology / skin-lesion care plan.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "dietitianCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's nutrition / weight-management care plans from the plan list.",
        "addPlan": "Create a new nutrition care plan for the patient.",
        "markServiceDone": "Mark a planned service as delivered from its row action in the services grid.",
        "addService": "Add a service row (consultation, meal plan, supplement, …) to the plan.",
        "removeService": "Remove the selected service(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the nutrition care plan.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "ophthalmologyCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's optical / glasses care plans from the plan list.",
        "addPlan": "Create a new optical / glasses care plan for the patient.",
        "inspectPrescription": "Focus an eye's row on the glasses-prescription anchor and read its refraction (sphere / cylinder / axis / add) per distance band.",
        "markProductDone": "Mark a planned product as dispensed from its row action in the products grid.",
        "addProduct": "Add a product row (lens, frame, coating, …) to the plan.",
        "removeProduct": "Remove the selected product(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the optical care plan.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "orthopedicsCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's orthopaedic / musculoskeletal care plans from the plan list.",
        "addPlan": "Create a new orthopaedic / musculoskeletal care plan for the patient.",
        "inspectInjury": "Focus a body region on the fracture / joint anchor and read its charted injury sites.",
        "markTreatmentDone": "Mark a planned procedure as executed from its row action in the treatments grid.",
        "addTreatment": "Add a planned procedure row to the current care plan.",
        "removeTreatment": "Remove the selected procedure(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the orthopaedic / musculoskeletal care plan.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "physioCarePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's physiotherapy care plans from the plan list.",
        "addPlan": "Create a new physiotherapy care plan for the patient.",
        "markSessionDone": "Mark a planned session as executed from its row action in the sessions grid.",
        "addSession": "Add a planned session row to the current care plan.",
        "removeSession": "Remove the selected session(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the physiotherapy care plan.",
        "addPlanNote": "Add a note to the plan via the docked Notes tool. A clinical note requires the clinical-note permission.",
        "triagePlanTasks": "Open or complete a follow-up task from the docked Tasks tool.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked Plan summary tool."
      }
    },
    "carePlanEditor": {
      "workflows": {
        "selectPlan": "Open one of the patient's care plans from the plan list. Activation is a native click on the plan row; the host loads the plan into the editor.",
        "addPlan": "Create a new care plan for the patient.",
        "inspectTooth": "Focus a tooth on the odontogram and read its charted dental status.",
        "openToothRadiographs": "Open the radiograph drawer for a tooth that carries imaging (the docked Control Room radiographs tool).",
        "markTreatmentDone": "Mark a planned treatment as executed from its row action in the treatments grid.",
        "addTreatment": "Add a planned treatment row to the current plan.",
        "removeTreatment": "Remove the selected planned treatment(s) from the plan (read data-table.selection for the targets).",
        "savePlan": "Persist the current edits to the care plan.",
        "addPlanNote": "Add a note to the plan via the docked Notes tool. A clinical note requires the clinical-note permission.",
        "triagePlanTasks": "Open or complete a follow-up task from the docked Tasks tool.",
        "regenerateSummary": "Regenerate the AI plan summary in the docked AI summary tool."
      }
    },
    "carePlans": {
      "workflows": {
        "selectPlans": "Select one or more plan or care rows in the register for a bulk action.",
        "filterByPeriod": "Filter the register to a created/scheduled date range via the per-column date filters.",
        "createPlan": "Open the \"New plan\" action from the page header and persist a new treatment plan for the patient.",
        "createAppointmentForCare": "From a planned care row action, schedule an appointment for that treatment.",
        "generateInvoice": "Bill a completed treatment: create the doctor bill / invoice the selected care sits on.",
        "exportCsv": "Download the current filtered register as a CSV from the page header.",
        "deleteCare": "Delete the selected non-billed care row from its plan. Irreversible."
      }
    },
    "confirmPhone": {
      "workflows": {
        "enterMobileNumber": "Type or edit the mobile number (Italy-default dial code, editable) in the phone field.",
        "confirmMobileNumber": "Save the entered mobile number, enabling the SMS two-factor path. An invalid or landline number keeps the prompt open with an inline error.",
        "skipPrompt": "Dismiss the prompt without saving. Access continues; the prompt re-appears at the next login.",
        "reopenPrompt": "Open the confirm-phone prompt again after it was skipped."
      }
    },
    "dashboard": {
      "workflows": {
        "triageWarnings": "Review the outstanding-action notices and dismiss the ones that are handled. The strip folds away once every notice is cleared. Dismissal is delegated through the WarningStack item `onDismiss` callback, so there is no imperative ui action.",
        "openInbox": "Open the inline-end AI tools rail to the Inbox tool to read the merged notifications / messages / product-update feed.",
        "quickCreate": "Use a quick-entry tile CTA to start creating a new patient, appointment, treatment plan, or invoice.",
        "navigateSection": "Jump to another workspace section (patients, schedule, documents, settings) from the Sidebar.",
        "reviewEfficiency": "Read the Studio efficiency gauge and open the settings checklist to complete the remaining configuration steps."
      }
    },
    "errorPage": {
      "workflows": {
        "readError": "Read which error the visitor hit (`data-status` on the root), what it means and the reference to quote to support, if shown.",
        "retry": "Try again (`data-action=\"retry\"`). By default this reloads the page. Inside an app the host may handle it instead and send the failed request again, such as a booking confirmation, so it counts as a write."
      }
    },
    "forgotPasswordForm": {
      "workflows": {
        "readForm": "Read where the request stands. The root carries `data-state`: `asking` while the form shows, `sent` once the confirmation has taken its place (`data-slot=\"confirmation\"`). While asking, the email sits in `data-field=\"email\"`, with `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element when it has a problem, also listed in `data-component=\"error-summary\"` after a refused submit; a message about the whole request is in `data-field=\"form\"`. The confirmation is the same for every address: it does not say whether an account exists.",
        "requestResetLink": "Ask for a password reset link (`data-action=\"submit\"`). The person types the email themselves; the form does not submit until it is a valid address. The host then sends the link, through its `onSubmit` handler or a native form post, and shows the confirmation for every address, whether or not an account exists for it.",
        "backToSignIn": "Go back to the sign-in page (`data-action=\"login\"`). Present only when the host gives its address."
      }
    },
    "helpMeWriteTemplates": {
      "workflows": {
        "runTemplate": "Open the Templates menu from the editor's AI pill, or from the bubble that appears over selected text, and pick a template (listed in the practice's order). It runs straight away and rewrites the selected text, or the whole note when nothing is selected; the notice that follows offers Undo. Without patient AI consent the consent banner shows first. Out of AI credits opens the Alia Writing Assistant on the out-of-credits notice.",
        "freePrompt": "Type (or dictate with the bar mic) what to write and press Write; the draft is inserted at the caret on Insert.",
        "manageTemplates": "Open Manage templates from the Templates menu in the AI pill: list the practice's templates, create or edit one (name, optional short description, instructions for the AI; names are unique), reorder them by dragging and delete one after a confirmation. Shown only when the host allows managing."
      }
    },
    "invoices": {
      "workflows": {
        "filterInvoices": "Narrow the ledger by number, relation, status, date range or amount using the per-column filters.",
        "sortLedger": "Re-sort the ledger by any column (e.g. issue date, total, balance).",
        "paginate": "Move to another page of the ledger.",
        "exportLedger": "Export the current (optionally filtered) ledger from the page-header table-tools cluster — CSV / Excel / PDF.",
        "viewInvoice": "Open a single invoice from its row action to inspect line items and payment history.",
        "downloadPdf": "Download the selected invoice as a PDF from its row action.",
        "sendReminder": "Send a payment reminder for the selected invoice through its eligible channel (email or SMS).",
        "deleteInvoice": "Delete the selected invoice from its row action. Paid invoices cannot be deleted. Irreversible."
      }
    },
    "loginForm": {
      "workflows": {
        "readForm": "Read the sign-in form and what is wrong with it. Each field sits in a wrapper with `data-field` (`email`, `password`, and `remember-me` when the host turns it on). A field with a problem has `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element inside the wrapper. After a refused submit the fields with a problem are also listed in `data-component=\"error-summary\"` at the top of the form, one item per field (`data-error-field`). A message about the whole form, such as a refused sign-in, is in `data-field=\"form\"`; it never says which of the two values was wrong. Never read or repeat the password.",
        "signIn": "Sign in (`data-action=\"submit\"`). The person types the email and the password themselves; the form does not submit until the email field is filled in and the password is not empty; text with an `@` must be a valid email address, and text without one is accepted as a username only when the host sets `allowUsername`; it lists what is wrong in its error summary and moves focus there. The host then signs the person in: through its `onSubmit` handler, or as a native form post to its own field names.",
        "rememberMe": "Tick or untick \"Remember me on this computer\" (`data-field=\"remember-me\"`, a checkbox, unticked by default). Present only when the host turns it on. It reports the choice to the host, which decides what it means; it changes nothing until the form is submitted.",
        "forgotPassword": "Go to the page that asks for a password reset link (`data-action=\"forgot-password\"`). Present only when the host gives its address.",
        "register": "Go to a registration page: `data-action=\"register-doctor\"` or `data-action=\"register-patient\"`, under the sign-in button. Each is present only when the host gives its address.",
        "devSignIn": "Development only: follow one of the quick sign-in links (`data-action=\"dev-login\"`, inside `data-component=\"login-dev-card\"`). Each leads to a route on the host that signs a test account in. The kit holds no test data. The card is present only when the host's server reports a development environment, and the server refuses those routes anywhere else."
      }
    },
    "marketplaceAppShell": {
      "workflows": {
        "navigate": "Navigate to one of the app sections via the sidebar nav (or the embedded tab strip). Routing is consumer-owned, so the destination resolves through the host router.",
        "toggleSidebar": "Collapse or expand the main navigation rail and read its current collapsed state.",
        "openAccountMenu": "Open the account dropdown to reach sign-out and account settings.",
        "setAppearance": "Change the active theme, locale, or accessibility profile from the header settings.",
        "connectAccount": "Start the AlfaDocs OAuth connect flow from the pre-auth screen. Persists an authorised session.",
        "signOut": "Sign out of the connected AlfaDocs account from the account menu. Ends the session."
      }
    },
    "patientAiSummary": {
      "workflows": {
        "openTool": "Open one of the rail tools — the Patient Summary brief or the Ask Alia chat — making its panel the open region inside the rail landmark.",
        "readBrief": "Read the compiled patient brief — overview counts/flags, plan state and progress, and the latest payment record.",
        "expandSection": "Expand one section of the brief accordion (overview, appointment, history, plan, or attention) to reveal its entity card.",
        "grantAiConsent": "Record the patient AI-assistance consent that gates the brief, revealing the structured summary once granted.",
        "askAlia": "Open the Ask Alia tool and send a question about this patient; Alia can answer with an in-conversation entity card."
      }
    },
    "patientCheckIn": {
      "workflows": {
        "signInWithCode": "Request a one-time SMS/email code, enter it, and authenticate into the check-in flow.",
        "setLanguage": "Pick the kiosk language for the check-in session.",
        "selectSubject": "Choose who is checking in (self or a dependent) and confirm their personal data.",
        "captureIdDocument": "Photograph or upload the health card / ID document for the visit.",
        "signConsents": "Read each consent form, add the signature, and submit the signed consents.",
        "payForVisit": "Pay the visit fee by card or digital wallet.",
        "completeCheckIn": "Finalise the check-in once all steps are done; marks the patient as arrived."
      }
    },
    "patientFolder": {
      "workflows": {
        "openSection": "Switch the folder to one of its section tabs — Anamnesis, Diagnosis, Care Plan, Communications, Documents or Bills — swapping the panel in place beneath the persistent patient banner. The banner and the four patient-level Control Room tools stay put across the switch. TabBar's active tab is a consumer-owned prop with no adapter action, so activation is a native click on the section button. View state only.",
        "readPatientBanner": "Read the structural summary the persistent patient banner carries — unsigned-document and highlighted-anamnesis counts, unpaid-balance and AI-consent flags — to decide what the folder needs attention on. Identifiers, counts and flags only; never patient identity or clinical content.",
        "customisePatientFields": "Choose which demographic, contact and address fields the folder shows, and in what order. The banner's field cog and the rail's Patient details tool share one field catalogue, so the choice applies to both. View state only — no patient data is written.",
        "openPatientTool": "Open, switch or close one of the Control Room tools docked in the inline-end rail. The four patient-level tools — Patient details, Notes, Appointments, Tasks — are folder-owned and available on every section tab; the section-scoped ones (Plan summary and Radiographs on Care Plan, Document preview on Documents) come and go with the active tab. View state only.",
        "updatePatientDetails": "Correct a patient demographic or contact field inline in the rail's Patient details tool — sex, date of birth, place of birth, fiscal code, profession, phone, email, address. Writes patient-identifiable data.",
        "addPatientNote": "Add a note to the patient from the folder's Notes tool. The folder has exactly ONE Notes tool, shared by every section tab, so a note filed from any tab lands on the same timeline. A clinical note requires the clinical-note permission — read it before writing. Writes patient-identifiable clinical content.",
        "openVisitNote": "Open the note recorded at one visit from the Diagnosis tab's evolution grid. The folder owns the address: the tab hands the visit's note id out and the folder routes it into its own Notes tool, which scrolls to and focuses that note — no second panel opens, so the note lives in exactly one place. Pressing the same marker again re-scrolls it. Read-only.",
        "reviewPatientAppointments": "Read the patient's appointment timeline in the rail — past and upcoming visits — and jump to today or focus one appointment. Read-only; booking happens in the Appointments context, not the folder.",
        "triagePatientTasks": "Work the patient's follow-up tasks in the rail: filter the list, jump to what is overdue, open a task or tick it done.",
        "reviewCarePlan": "Read the patient's open care plan in the Care Plan tab — the plan header's lifecycle state and alerts, the charted odontogram, the treatments grid and the rail's plan summary. The folder mounts the shared care-plan-editor surface with its own patient card suppressed, so the folder banner remains the only patient header. Editing the plan is the Care Plan Editor pattern's contract, not the folder's.",
        "openToothRadiographs": "Open the films charted against one tooth. This is a folder-specific seam: clicking a tooth on the odontogram routes that tooth's radiographs into the FOLDER's Control Room rail (alongside the pinned patient tools) rather than into a plan-local drawer. Read-only.",
        "reviewAnamnesis": "Read the patient's anamnesis from the folder: the Anamnesis tab mounts the tool's intake surface (the pre-visit questionnaire) and the Diagnosis tab mounts its compile surface (the diagnostic record plus the across-visits evolution grid), both embedded so they sit flush under the folder header. Switch between the tool's own summary / compile / evolution views. Recording measurements, targets and answers is the Anamnesis pattern's contract, not the folder's.",
        "browseDocuments": "Navigate the patient's document tree in the Documents tab: open a folder, filter by document type, and select files. Reads structural metadata only — ids, kinds, sizes, dates, document types and signature states. Document NAMES can identify a patient and are not returned here.",
        "previewDocument": "Open a document into the Control Room rail's Document preview. Single-clicking one file previews it; selecting two or more surfaces them as a tabbed preview. Reads only which render branch is active (pdf, image, metadata or empty) — never the document name. Read-only; nothing is downloaded or shared.",
        "identifyDocuments": "Resolve the display names of the selected documents, or of the one in the rail preview, when the agent must name a file back to the user. Document names routinely identify a patient, so this is the deliberate, audited read — every other document readback stays name-free.",
        "uploadDocument": "Upload one or more files into the open folder of the patient's document tree. Writes a patient-identifiable document.",
        "organiseDocuments": "Reorganise the document tree: create a folder, rename an item, or move the selected items into another folder. Only one of these action dialogs can be in flight at a time — read the active action before starting another. Reversible through the same toolbar.",
        "deleteDocuments": "Delete the selected files or folders from the patient's document tree. Deleting a folder takes everything nested beneath it. Not reversible from this surface — read the selection first and confirm.",
        "requestDocumentSignature": "Send one document out for the patient's signature. A pending document's Request signature row action opens the signature request modal with that document's signed versions table above the request form; for Via QR, the QR step follows in the same dialog (the full contract is signature-request.agent.ts). The banner's unsigned-documents count is the same signal, so read it to see what is already pending before requesting another. Puts a patient-identifiable document into an outbound signing envelope.",
        "requestRequiredSignatures": "Ask the patient to sign every outstanding item in one request. The banner's Documents to sign button (a user-driven button with no adapter action) opens the signature request modal in required documents mode: the unsigned documents and anamnesis interviews, each with its own operator picker, one signer roster for the batch, and a link sent outcome after submit. A signer the host marks as under 18 shows a warning that never blocks the request. Read the banner's counts first to know what will be listed.",
        "shareDocuments": "Share the selected documents with the patient. The shared files then appear as a document-share bubble in the Communications thread. Sends patient-identifiable documents out of the practice.",
        "readMessageThread": "Read the per-patient message thread in the Communications tab — App, SMS, Email and inbound WhatsApp messages in one chronological log, with SMS delivery status and campaign / workflow / document-share provenance on each bubble — and scroll back to the latest message. Read-only.",
        "composeMessage": "Draft a message to the patient: pick the outbound channel (App, SMS or Email — WhatsApp is inbound-only and never offered) and type the body. On SMS the composer counts GSM/Unicode segments including the appended practice sender name, so read the draft back before sending a long one. Nothing leaves the practice until the draft is submitted.",
        "sendMessage": "Send the composed message to the patient over the picked channel and append it to the thread. The destination readout beside the channel picker names exactly where it lands — portal account, mobile number or email address — so confirm it before submitting. Sends patient-identifiable content out of the practice.",
        "enablePatientChannel": "Turn on a communication channel the patient has no route for yet — no portal account, no mobile number, no email address. The composer shows an enablement prompt in place of offering that channel; enabling it adds the channel back to the picker. Requires recording the patient contact detail the channel needs, so it writes patient-identifiable data."
      }
    },
    "patientPortal": {
      "workflows": {
        "signInWithCode": "Request a one-time SMS/email code, enter it, and authenticate into the portal.",
        "navigateSection": "Switch between the Appointments, Documents, and My details portal sections.",
        "checkIn": "Start the day-of check-in for an upcoming appointment from the portal home.",
        "cancelAppointment": "Cancel an upcoming appointment after confirming in the dialog. Frees the slot.",
        "uploadDocument": "Upload a file (PDF/JPG/PNG) to the patient document library.",
        "saveDetails": "Edit and save the patient personal details (contact, address, fiscal code)."
      }
    },
    "patientRecord": {
      "workflows": {
        "openPatientTool": "Switch the Control Room rail between the patient tools — the editable \"Patient details\" panel and the AI summary — or collapse the rail back to its icon strip. View state only; no panel content is exposed.",
        "readRecordStatus": "Read the structural state of the record already on screen — which patient the panel is bound to, which field keys are visible and in what order, and the card’s counts and flags (unpaid balance, unsigned documents, highlighted anamnesis answers, AI consent). Returns identifiers, counts and field KEYS only, never a field value.",
        "customizeFieldSet": "Show, hide and reorder the patient fields through the details panel’s cog picker. The one preference drives BOTH surfaces at once, so the main-area summary card reflects the change immediately. Mutates the per-user view preference, not the patient record, and is reversible with the same picker.",
        "updatePatientField": "Write a patient field value from the details panel — an identity or contact field, or a clinical field such as the diagnosis note. Routes through the consumer’s audited save path, so the same permissions, validation and audit apply as to a human inline edit. Writes patient-identifiable data.",
        "copyPatientIdentifier": "Copy a sensitive identifier (e.g. the fiscal code) out of the details panel to the clipboard. The copy affordance lives inside the field row and exposes no adapter action, so this workflow documents the boundary rather than driving it: the value leaves the surface, so it is treated as a PHI egress and must be confirmed and audited.",
        "refreshAiBrief": "Open the AI summary tool in the rail and request a fresh brief over this patient, then read back when it was generated and the shape of its sections (headings, tone, item counts — never the bullet text). Generation is gated upstream by the AI consent banner. Produces patient-identifiable clinical narrative, so it is stricter than EntitySummary’s own entity-agnostic write."
      }
    },
    "patients": {
      "workflows": {
        "filterFromNotice": "Apply a WarningStack notice’s \"View\" action to narrow the roster to the flagged patients (e.g. overdue balances, estimate-stage plans).",
        "selectPatients": "Select one or more patient rows in the roster for a bulk action.",
        "sendReminders": "Send an appointment or balance reminder to the selected patients through their eligible channel (email or SMS).",
        "tagPatients": "Add a tag to every selected patient.",
        "deletePatients": "Delete the selected patients. Archived patients cannot be deleted. Irreversible.",
        "exportRoster": "Export the current (optionally filtered) patient roster from the page-header table-tools cluster — CSV / Excel / PDF."
      }
    },
    "paymentAutomationUpsell": {
      "workflows": {
        "viewUpsell": "Read the Payment Automation value preview: the how-it-works steps, the sample payment-request list, and the included-features checklist.",
        "activateModule": "As the practice owner, activate the Payment Automation module on the subscription. Persists a billing change.",
        "notifyAdmin": "As a non-owner member, notify the practice owner / admin to activate the Payment Automation module."
      }
    },
    "periodontalCharting": {
      "workflows": {
        "reviewChart": "Read the exam back without changing it: the derived whole-mouth indices (mean probing depth, mean attachment level, plaque %, bleeding on probing %, scored-site and deep-pocket counts) and the narratable per-tooth problem list, ordered clinically with each tooth’s pockets deepest-first — e.g. “46: 8 mm disto-buccal pocket, bleeding, grade 2 mobility”. Structural readings and FDI ids only; the free-text per-tooth note is never returned.",
        "recordProbingDepths": "Walk the mouth site by site during probing: move focus to a measurement input, then record the pocket depth and the gingival recession in whole millimetres at that site. Writes patient clinical data.",
        "recordToothFindings": "Record the non-millimetre findings alongside the depths: bleeding on probing and plaque at a site, the tooth’s Miller mobility grade (0–3), and whether the tooth is present or absent. Writes patient clinical data.",
        "readRiskProfile": "Read the Lang & Tonetti risk hexagon beside the chart: the overall periodontal risk verdict (low / moderate / high) and the per-factor bands (bleeding, pockets, tooth loss, bone loss vs age, smoking, systemic). Ordinal bands only — never the raw clinical inputs. The hexagon is display-only, so there is nothing to drive here.",
        "recordDiagnosis": "Record the 2017-classification diagnosis for the exam: stage (I–IV), grade (A–C), extent (localised / generalised / molar-incisor) and the ticked contributing factors, then read back the composed diagnosis sentence and whether the form is complete. Writes patient clinical data; the clinician’s free-text note is never exposed.",
        "saveExam": "Persist the whole periodontal examination — every probing reading plus the recorded diagnosis — against the patient record once charting is finished. Writes patient clinical data.",
        "clearToothReadings": "Discard every reading captured on one mischarted tooth — all six sites, the mobility grade and the missing / implant flags. Not undoable from this surface; confirm the FDI id with the clinician first.",
        "clearDiagnosis": "Reset the recorded stage, grade, extent and contributing factors back to empty, leaving the chart readings untouched. Not undoable from this surface."
      }
    },
    "prenota": {
      "workflows": {
        "searchPractitioners": "Pick a specialty / clinic and a location in the hero search comboboxes and submit to find matching practitioners.",
        "browseResults": "Scroll the search results list and focus a map pin to locate a practice geographically.",
        "openPractice": "Open a practice or operator profile from a result card to view contact details and reviews.",
        "filterReviews": "Filter the reviews panel on a profile by star rating to read targeted feedback.",
        "bookVisit": "Start booking a visit from a result card next-available slot or a profile \"Book visit\" CTA.",
        "navigateMenu": "Open the public header navigation (doctors, pricing, for-practices) or the sign-in / register actions.",
        "chooseService": "Choose the service to book on the booking page. Type into the service combobox to narrow the practice catalogue, commit the matching service id, then move the flow on. Reading `combobox.query` back confirms what was actually typed; `combobox.value` confirms what stuck. This is the step patients abandon most, so prefer setting the service id directly over leaving a half-typed filter.",
        "chooseOperator": "Narrow the booking to particular practitioners using the operator multi-select, or clear it back to \"no preference\". An empty selection means any operator — clearing is the correct way to widen availability, not a failure.",
        "pickSlot": "Find and select an appointment slot in the four-day grid. Read `status` before concluding there is no availability — `loading` means the window has not resolved. Read `slotsByDay` for the bookable ids in view (it already includes slots collapsed behind \"Show more\", so they can be selected without expanding), page with `next_days` / `previous_days` while `hasNext` / `hasPrevious` allow it, then commit with `select_slot`. Selecting a slot only fills the booking draft — it does not book anything.",
        "submitBookingRequest": "Commit the booking request once a service, slot and patient details are in place. The commit goes through the appointment tool layer, not the form: the details fields (name, email, mobile, optional date of birth and fiscal code, consents) have no adapter and are not addressable through the registry, so send the payload to `appointment.book` rather than trying to type into the form. `goto_step` is only for walking the patient back to a step they left incomplete.",
        "confirmBookingRequest": "Confirm a pending booking request from the single-use link the patient is emailed (`/conferma?token=…`). The landing page confirms on load and only reports the outcome, so there is no UI to drive — redeem the token through the API. A token is single-use: a second attempt reports \"expired\", which is the correct terminal state, not an error to retry.",
        "abandonBooking": "Clear an in-progress booking back to an empty flow. Destructive: everything the patient has entered — service, operator, slot and any details already typed — is lost and cannot be recovered from the UI. Confirm with the patient first."
      }
    },
    "registrationForm": {
      "workflows": {
        "readForm": "Read which fields the form asks for and which have a problem. The host picks the fields, so read them from the page: each one that shows sits in a wrapper with `data-field`. A field with a problem has `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element inside the wrapper. After a refused submit the fields with a problem are also listed in `data-component=\"error-summary\"` at the top of the form, one item per field (`data-error-field`). A message about the whole form is in `data-field=\"form\"`. An email the person cannot change is marked `readonly`. When the host asks for a code, a field's wrapper also carries `data-verification` (`idle`, `sent` or `verified`); a verified field is `readonly`. When the address already has an account, the email message says so and the two link-buttons under it lead on: sign in (`data-action=\"sign-in\"`) or reset the password (`data-action=\"reset-password\"`). Never read or repeat the password fields.",
        "register": "Create the account (`data-action=\"submit\"`). The person fills in the fields and accepts the terms themselves; the form does not submit until every field is valid and, when the host asks for codes, until the email and the phone are verified; it lists what is wrong in its error summary and moves focus there. The host then registers the account: through its `onSubmit` handler, or as a native form post to its own field names.",
        "verifyContact": "Prove an email or a phone number with a code, when the host asks for it (a field wrapper has `data-verification`). Press Send code (`data-action=\"send-code\"`) beside the field; the code goes to the person's own inbox or phone, so the person reads it and types it into the code box (`data-component=\"otp-input\"`), or presses Verify (`data-action=\"verify-code\"`). Send a new code is `data-action=\"resend-code\"` and waits a few seconds; `data-action=\"change-value\"` returns to editing. Never read or guess the code. When it is right the field becomes `readonly` and `data-verification` is `verified`. The host's server holds the verified value and checks it again on submit.",
        "devBypass": "Development only: press the shortcut (`data-component=\"dev-bypass-button\"`). It puts the values the host supplied into the fields that show, as if typed, and submits the form through the normal path when the host asked for that; a host may instead run a handler of its own, such as a development-only route. The kit holds no test data. The button is present only when the host's server reports a development environment, and what it submits is checked by the server like any other submit."
      }
    },
    "resetPasswordForm": {
      "workflows": {
        "readForm": "Read where the reset stands. The root carries `data-state`: `form` while the passwords are asked for, `done` once the password has changed, `expired` when the link no longer works. The result (`data-slot=\"result\"`) replaces the form. In the form, the address the password is for may show read-only in `data-field=\"email\"`; the new password is in `data-field=\"password\"` with its rules checklist, and its repeat in `data-field=\"passwordConfirm\"`. A field with a problem carries `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element, also listed in `data-component=\"error-summary\"` after a refused submit; a message about the whole form is in `data-field=\"form\"`.",
        "changePassword": "Choose a new password (`data-action=\"submit\"`). The person types both passwords themselves; an agent never types or reads a password. The form does not submit until the password meets every rule and the two entries match. The host then changes the password, through its `onSubmit` handler or a native form post, and shows the `done` state.",
        "signIn": "Go to the sign-in page once the password has changed (`data-action=\"login\"`). Present only in the `done` state, and when the host gives its address.",
        "requestNewLink": "Go to the page that asks for a new reset link (`data-action=\"request-link\"`). Present only in the `expired` state, and when the host gives its address."
      }
    },
    "roiDashboard": {
      "workflows": {
        "viewFunnel": "Read the patient-journey funnel — per-stage efficiency, time saved, and potential for the selected period.",
        "setPeriod": "Change the reporting period for the whole funnel; every cubotto and KPI recomputes.",
        "drillIntoStage": "Open a stage cubotto to its per-KPI drill-down breakdown (and the AI insight cubotto to the ROI overview).",
        "switchRole": "Switch the dashboard to another role via the multi-role tab switcher and read the active role.",
        "openInbox": "Open the inbox rail's notifications/messages tool and read which tool is active.",
        "exportKpi": "Export a KPI chart as a PNG for a report, or request a full ROI report export.",
        "dismissAlert": "Dismiss an item from the top-of-page alerts strip. Persists the dismissal so it stays hidden."
      }
    },
    "sendRegistrationLinkForm": {
      "workflows": {
        "readForm": "Read which fields have a problem. Each field sits in a wrapper with `data-field`; a field with a problem has `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element. After a refused submit the fields with a problem are listed in `data-component=\"error-summary\"`. After a send the root has `data-state=\"sent\"` and the result is in `data-slot=\"sent\"`.",
        "sendLink": "Send the registration link (`data-action=\"submit\"`). The person fills in the email twice, a mobile phone twice and the last day login is allowed; the form does not submit until every field is valid. The host then sends the email through a native form post or its `onSubmit` handler."
      }
    },
    "services": {
      "workflows": {
        "filterServices": "Narrow the catalogue by name, code, category, VAT rate or active status using the per-column filters.",
        "sortCatalogue": "Re-sort the catalogue by any column (e.g. price, duration, active).",
        "compactActions": "Toggle the ColumnToggle settings-footer switch to compact the per-row action column.",
        "toggleActive": "Flip a service’s availability via its per-row Active toggle so it is offered (or withdrawn) on new plans.",
        "exportCatalogue": "Export the current (optionally filtered) catalogue from the page-header table-tools cluster — CSV / Excel / PDF.",
        "editService": "Edit the selected service (e.g. price, duration, VAT) from its row action.",
        "duplicateService": "Duplicate the selected service into a new editable copy from its row action.",
        "deleteService": "Delete the selected service from its row action. A service in use on a plan cannot be deleted. Irreversible."
      }
    },
    "settings": {
      "workflows": {
        "openSection": "Open a settings section from the Hub or the grouped section nav. Read-only navigation.",
        "editTextSetting": "Set a text field in the active section (e.g. practice name, booking address, bank IBAN); marks the form dirty and enables Save.",
        "toggleSetting": "Turn a boolean setting on or off (e.g. yearly numbering, group services).",
        "chooseOption": "Pick a single option from a select or radio group (e.g. tax regime, document type, stamp method).",
        "setMultiChoice": "Select or clear members of a multi-choice group (e.g. accepted insurers, active branches, per-area permissions).",
        "pickColour": "Choose a colour from the practice palette (e.g. booking page accent).",
        "saveSection": "Persist the dirty section form. Save is disabled until a field changes.",
        "addRecord": "Open the create dialog for a list section (cash & banks, chairs, payment types…) and persist a new record.",
        "deleteRecord": "Remove a record from a list section after confirmation. Irreversible.",
        "filterRecords": "Filter the rows of a list section by a column value.",
        "uploadPracticePhoto": "Upload, crop, or remove the practice / profile photo.",
        "copySecret": "Copy a read-only secret to the clipboard (API key, data-import key, support PIN, iCal feed URL).",
        "revokeApiKey": "Revoke or regenerate an API key. Apps using the old key lose access immediately.",
        "connectOnlinePayments": "Connect or disconnect the Stripe account used for online payments.",
        "deleteArchive": "Delete the current archive after a password-gated confirmation. Permanently removes all of its data. Irreversible.",
        "openTool": "Open or switch a Control-Room rail tool (inbox, Alia) without leaving Settings.",
        "collapseSidebar": "Collapse or expand the primary navigation sidebar."
      }
    },
    "signatureRequest": {
      "workflows": {
        "reviewSignatures": "When the host passes `signatures` (or `signaturesLoading`), the request form shows the document's past signature requests at its top, on the same screen as the operator and signers, as on platform: a table named Signed versions (section data-section=\"signed-versions\") with the columns Date, Signature type and Actions and one row per request (data-signature-id), newest first. Date is the weekday, date and time in the active locale; Signature type is the legal level (Simple or Advanced, hidden with showLegalLevel=false), the channel (SMS, e-mail, WhatsApp, on this device or Via QR) and a SignatureStatusChip whose tooltip names who signed, who is still awaited and who signed on a party's behalf. Loading shows skeleton rows; no records shows one line saying there are no signed versions. Hosts that pass neither prop get the plain form.",
        "viewSignature": "Open one signed document from its row's View button; the host receives onViewSignature(id). The button shows only when the host passes that callback.",
        "downloadSignature": "Download one signed document from its row; the host receives onDownloadSignature(id, 'full' | 'anonymised'). When the record has an anonymised version the Download button opens a menu with Full version and Anonymised version (menu id signature-download-<record id>); otherwise it asks for 'full' directly. The button shows only when the host passes that callback.",
        "shareSignature": "Share one signed document from its row's Share menu (menu id signature-share-<record id>): link, e-mail or WhatsApp, in that order. The host receives onShareSignature(id, 'link' | 'email' | 'whatsapp') and does the sharing. The host decides which targets a row offers: the record's shareTargets, else the modal's signatureShareTargets, else all three. The button shows only when the host passes that callback and the row has at least one target.",
        "deleteSignature": "Delete one signed document from its row's Delete button. It opens a confirmation, an alert dialog (dialog id signature-delete-<record id>) asking whether to delete this signed document, with the row's date under the question and Cancel and Delete. Only Delete calls onDeleteSignature(id); the host deletes and drops the record. Cancel, Esc or dialog.close / dialog.dismiss close it without deleting, and focus returns to the row's Delete button. Once the host drops the record, focus moves to the next row's first action (else the previous row's, else the line saying there are no signed versions) and a polite status says the signature was deleted. The button shows only when the host passes that callback.",
        "assignOperator": "Pick the operator who must counter-sign (or \"Assign later\" to leave the request unassigned). The operators are listed in host order, then \"Assign later\". An operator the host marks disabled shows greyed out with the host's reason under the name (the trigger is described by every greyed-out operator and reason) and cannot be picked; set_value refuses them. An operator the host marks deleted is listed only while they are the current value, and is never seeded. \"Assign to me\" claims it for the signed-in operator.",
        "manageSignerRoster": "Build the patient-side signer roster: pick who signs in each row (the patient or their configured signee, e.g. a legal guardian), add a second signer up to the cap, or remove a row. People already on the roster are excluded from the other rows; set_value with one of them, or anyone not offered in that row, leaves the row unchanged. In required documents mode the roster signs the whole batch: the main signer (row data-signer-role=\"main\"), then at most one second signee (data-signer-role=\"second\", a group named Second signee). The second signee is offered only when every listed item is a document that can carry a second signature (anamnesis interviews never can), the main signer's channel is on this device (the batch channel with channelMode batch, else the main signer's own row), and the main signer is a registered signee, not the patient (isPatient); otherwise Add signer is disabled and a visible line says why. The patient is never offered as the second signee. Changing the main signer removes the second signee, announced via a live region. Moving the main signer's channel off this device only hides them (they are not submitted) and announces why; back on this device they return. Per signer, the second signee has their own channel beside their name. A second signee who is the main signer again, is no longer on offer, or cannot receive their own channel (the batch channel in batch mode) shows a message under their row (data-note=\"second-signee-issue\") and keeps Request signature disabled. When the host marks the patient (the isPatient option) as under 18 (isMinor), a warning under the Patient signers heading (data-note=\"minor-patient\", data-warning addSignee, selectSignee, selectSecondSignee or addSecondSignee) gives platform's advice from the roster: with no registered signee on the roster, add a parent or guardian (none registered) or choose one (one registered); with one signee and a free second seat, have the other parent or guardian sign too (another registered) or add them (none). With two signees, or no second seat, it says nothing. Any other person marked isMinor gets a general reminder in their own row (data-note=\"minor-signer\"). Neither blocks the request, and the kit does not compute age. A link-style Add signatory button calls the host's onAddSignee: inside the minor patient warning when it asks to add someone, and in required documents mode under the Patient signers heading while the patient has no registered signee (no signer option without isPatient), only once. Add signatory is user-driven with no adapter action, and the host adds the new person to signerOptions. With signerAddAction createSignee, the button beside the Patient signers heading is Add signatory (data-action=\"create-signee\") and calls onAddSignee instead of adding a row; the minor patient warning then has no button. With secondSigneeSlot (request form, maxSigners 2 or more), an Add signer button beside the heading (data-action=\"add-second-signee\") opens a line (data-second-signee-slot) with a select named Second signee; it shows only while one can be seated (a registered signee is the main signer and another is registered). With secondSigneeSlot or signerAddAction createSignee, every signer dropdown ends with a New signatory option that calls onAddSignee (the host's create-signee page) and leaves the row unchanged. New signatory is user-driven; set_value refuses it. The line offers registered signees only and has a Remove signer button that takes it away again; picking one seats the second signee (data-signer-role=\"second\"), whose Remove signer also takes the line away. While the patient is the main signer a visible line under the heading says why there is no second signee (data-note=\"second-signee-hint\"). A new main signer clears the second signee, announced via a live region.",
        "chooseDeliveryChannel": "Choose how each signer's request is delivered in the channel dropdown beside their name, a select named Signature channel (one choice): SMS, e-mail, WhatsApp, signing on this device, or via QR (the signer scans a QR code the dialog shows in its QR step after the request; offered to every patient-side signer by default). Each signer row (data-signer-id) holds its dropdown, with the id signature-request-signer-channel-<row number>; the operator dropdown's id is signature-request-operator-channel. Each option carries data-option-id with the channel; set_value with a channel the dropdown does not offer or has disabled leaves the choice unchanged. A signer's available channels can hide any of these, Via QR included. A signer's unavailableChannels instead show a channel greyed out with the host's reason under it (e.g. SMS and WhatsApp with No mobile number): set_value refuses it, the row never falls back to it, and a signer with no channel left shows the reasons under the row and keeps Request signature disabled. When the host gives a greyed-out channel an actionHref, a link under the person (data-note=\"contact-actions\"), such as Add mobile number or Add e-mail address, leads to the host's page to fix it; in batch mode the main signer's links show under the batch channel select. They are plain links with no adapter action. On-device signers sign sequentially in roster order. The assigned operator has the same dropdown, named Operator signature channel, with SMS, e-mail, or on this device, WhatsApp only when the host rule lists it in operatorAvailableChannels (as platform does for operator-only documents), and never Via QR: the operator's own system-defined channel (defaultChannel, from their settings) arrives preselected and follows a newly picked operator, while a channel changed by hand is kept until the operator changes; the on-device option is enabled only while a patient signer also signs on this device (while disabled, its reason shows under the option and describes the trigger; an automatic fallback to the operator's default is announced via a live region when the last on-device patient row leaves). A host can replace that rule with resolveChannels: it receives the choices after every channel change (and on open) and returns the channels to use and to offer, e.g. the operator and the patient on this device together; set_value may then move another person's channel too (the operator or another signer), announced via the same live region. Channels are per signer by default, in the request form and in required documents mode. With channelMode batch there is no channel beside each name: one labelled Signature channel select (id signature-request-batch-channel) under the signers heading sets the channel for the whole roster, with SMS, e-mail, WhatsApp and Via QR offered when the main signer or the patient (isPatient) can receive them, greyed out only when neither can, and on this device always offered. It starts on e-mail, or SMS when the main signer is the patient and can receive it, unless the host passes initialBatchChannel; in required documents mode changing the main signer returns it to that default (initialBatchChannel is read only on open), announced via the live region. resolveChannels receives the batch channel as every signer's channel and moves the batch through the main signer's returned channel. In required documents mode with showOperatorChannels, each item with an assigned operator also has that operator's channel select (SMS, e-mail or on this device), named Operator signature channel for <item name> with the id signature-request-item-operator-channel-<item id>; it starts on the operator's defaultChannel and follows a newly picked operator (Assign to me included), keeping a channel changed by hand until then. Items on Assign later have none. With batchChannelPlacement inline, the batch select (same id) sits in the main signer's row instead of under the heading, and any other signer shows the batch channel as read-only text (data-note=\"batch-channel\"). With linkOnDevice, the operator signs on this device exactly when the patient side does: set_value On this device on the operator's channel also moves the main signer (or the batch) there, while the patient side is on this device the operator's other channels are disabled with the reason, and when it leaves the operator returns to their default, each announced via the live region.",
        "waiveOtp": "Toggle a signer's on-device OTP waiver. Waiving OTP lowers the evidentiary value of that signature; the pattern surfaces a warning alert while waived. With channelMode batch there is one switch for the whole roster (Do not require OTP from the patient), shown only while the batch channel is on this device: it starts on when the patient cannot receive SMS, whoever the main signer is, and is on and disabled when the main signer is the patient and cannot receive SMS. The choice resets when the batch channel changes. Its warning (batchOtpWarning) is one batch-level alert outside every signer row; as on platform it shows while the waiver is on, and on every channel while it is forced. Per signer in required documents mode, the main signer's own switch follows the same rules (starts on, or on and disabled, described by the warning) and resets when that row's channel changes; other rows are the user's choice.",
        "requestSignature": "Send the signature request. Notifies every roster signer over their selected channel and hands the submission to the host. With confirmRecipients, when anyone is reached by SMS, e-mail or WhatsApp, Request signature first shows the recipient check (see confirm_recipients) and nothing is handed over until Send. When any signer chose Via QR the dialog stays open and moves to its QR step. With channelMode batch the submission carries signMode (the batch channel, also on every signer). In required documents mode it also carries operatorAssignments (item id to operator id, null for assign later), requiredItemIds and, with showOperatorChannels, operatorChannels (item id to the operator's channel, for assigned items), with operatorChannel null; the main signer OTP waiver is skipOtp on the main signer, set only on this device. While the host passes loading, the dialog shows skeleton rows (data-note=\"loading\", aria-busy) and Request signature is disabled; the form seeds when loading ends.",
        "confirmRecipients": "With confirmRecipients, a request that reaches anyone by SMS, e-mail or WhatsApp first shows a step headed \"Send the signature request?\" listing each recipient (list items with data-recipient=\"operator\" or \"signer\"): the assigned operator in the request form, then every roster signer on a remote channel, with their channel and the phone number or e-mail address the host gave as their contact. Send hands the submission to the host; Back returns to the form with nothing sent. On this device and Via QR need no check. The step stays while the host is busy; a new error returns to the form and an outcome replaces it. Send and Back are user-driven buttons with no adapter action; the contact is shown as text only, never in a DOM hook.",
        "showQrCode": "After a request with a signer on the Via QR channel, the same dialog shows its QR step: a loading state until the host passes the server-issued links, then one code per QR signer with the signer name and expiry (the code or its error carries data-signer-id). With several QR signers, the user moves between codes with Previous and Next (\"1 of 2\"); Done on the last code tells the host the codes were handed over (onQrDone) and closes the dialog. Previous, Next and Done are user-driven buttons with no adapter action. Closing through dialog.close, or dismissing the waiting state with dialog.dismiss, does not fire onQrDone. A QR signer with no link shows an error in place of the code. The operator is never offered Via QR.",
        "reviewRequiredItems": "When the host passes `requiredItems` (or `requiredItemsLoading`), the form asks one patient to sign every outstanding item in one batch: documents to sign and anamnesis interviews to complete, as two lists named with their counts. Each row (data-required-item-id) shows the item name only, with no signing status: the documents API of the host says only whether an item is signed. Items marked signed are left out; when none is left the dialog shows the all signed outcome. Loading shows skeleton rows. The single operator row and the signed versions table do not appear in this mode.",
        "assignItemOperators": "In required documents mode, pick the operator for each item that needs an operator signature (requiresOperator), or \"Assign later\", listed first. Each picker is named \"Operator for <item name>\" and has the id signature-request-item-operator-<item id>. Greyed-out and deleted operators follow the same rules as the single operator picker: a disabled operator shows its reason and set_value refuses them, a deleted one shows only in a picker still assigned to them. \"Assign to me\" (shown when the host passes currentOperatorId) sets every picker to the signed-in operator; it is a user-driven button with no adapter action. An info note shows while any picker is on Assign later, and another says each operator receives the request by e-mail or SMS per their own settings, unless the host passes showOperatorChannels: then each assigned item shows its operator's channel instead (see choose_delivery_channel).",
        "showOutcome": "When the host passes `outcome`, the dialog replaces every other view with a final screen whose heading and description carry the message, and focus moves to the heading: 'linkSent' (the secure link went out; in required documents mode the heading says the documents were sent to the patient), 'allSigned' (nothing left to sign) or 'creditsExhausted' (no electronic signature documents left). Close ends it; on 'creditsExhausted' a Buy more documents button calls onBuyMoreSignatures when the host passes it, and a Cancel button, when the host passes onOutcomeCancel, returns the dialog to the form and calls it so the host clears outcome (both user-driven, no adapter action).",
        "dismiss": "Close the modal without sending a signature request. Discards the in-progress operator, roster, channel, and OTP choices."
      }
    },
    "signedOutFrame": {
      "workflows": {
        "readPage": "Read the page a signed-out visitor is on. The page's content is inside `<main>` (the header's skip link points at it). The header (`role=\"banner\"`) holds the brand link and, when the host gives their addresses, links to the other signed-out pages: `data-action=\"login\"`, `data-action=\"register-doctor\"`, `data-action=\"register-patient\"`. There is no search, inbox or account menu before sign-in. The footer (`role=\"contentinfo\"`) links to the privacy policy and the terms.",
        "goToSignIn": "Follow the header's \"Sign in\" link (`data-action=\"login\"`). Present on a registration or forgot-password page, when the host gives its address.",
        "goToRegistration": "Follow one of the header's registration links: `data-action=\"register-doctor\"` or `data-action=\"register-patient\"`. Present on the login page, for each address the host gives. Each link is in the page twice, once with its label for wide screens and once as an icon for narrow ones, and only one of the two is shown: follow the one that is visible."
      }
    },
    "signing": {
      "workflows": {
        "verifyIdentity": "Pass the identity gate before the document is shown: enter the 5-character SMS code, or tick the confirm-identity checkbox when no mobile number is on file, then verify.",
        "readDocument": "Page through and zoom the document under signature before signing each field.",
        "signField": "Activate a signature / initial / date field overlay, draw or type the signature in the capture dialog, and confirm it. Signing the last field auto-advances to the success terminal.",
        "completeEnvelope": "Finalise the envelope once every required field carries a signature (with two signees: once both the primary and the secondary have signed all of their fields), persisting the completed document.",
        "verifySigneeIdentity": "The active signee (primary or secondary) passes its own identity gate: enter the 5-character SMS code, or tick the confirm-identity checkbox when no mobile number is on file, then verify.",
        "signSigneeFields": "The active signee signs the surface filtered to its own fields: activate a field overlay, draw or type the signature in the capture dialog, and confirm it.",
        "handOffToSecondarySignee": "After the primary signee finishes, hand the device to the secondary signee (same-device flow), switching the active session and re-filtering the surface to the secondary signee’s fields."
      }
    },
    "tasks": {
      "workflows": {
        "filterTasks": "Narrow the roster by title, type, assignee, due range or status using the per-column filters.",
        "filterFromNotice": "Apply a WarningStack notice’s \"View\" action (e.g. \"View overdue\", \"View collections\") to narrow the roster to the flagged tasks.",
        "sortRoster": "Re-sort the roster by any column (e.g. soonest due first, created date).",
        "toggleDone": "Mark a task as done (or reopen it) via its per-row done toggle.",
        "compactActions": "Toggle the ColumnToggle settings-footer switch to compact the per-row action column.",
        "exportRoster": "Export the current (optionally filtered) roster from the page-header table-tools cluster — CSV / Excel / PDF.",
        "editTask": "Edit the selected task from its row action.",
        "deleteTask": "Delete the selected task from its row action. Completed tasks cannot be deleted. Irreversible."
      }
    },
    "twoFactorLogin": {
      "workflows": {
        "enterCode": "Type the 6-digit verification code into the OTP field.",
        "verifyCode": "Submit the entered code to complete the second-factor challenge and sign in.",
        "resendCode": "Request a fresh SMS code (capped at the configured resend limit).",
        "switchMethod": "Switch between the SMS and authenticator-app verification methods, resetting the entry."
      }
    },
    "upgradePractice": {
      "workflows": {
        "viewPackages": "List the available upgrade packages with their headline benefits and price.",
        "openPackage": "Open a package card to its detail view with the quantified benefit rows.",
        "configurePackage": "In the basket, set the quantity or tier for a tiered/quantity package and read back the chosen value; the monthly-price-increase summary recomputes.",
        "confirmUpgrade": "Confirm the upgrade — add the configured package to the practice subscription and bill the prorated partial period. Persists a billing change."
      }
    },
    "warehouse": {
      "workflows": {
        "filterInventory": "Narrow the inventory by product, SKU, category, supplier or stock status using the per-column filters.",
        "sortInventory": "Re-sort the inventory by any column (e.g. in-stock, total value, last restocked).",
        "compactActions": "Toggle the ColumnToggle settings-footer switch to compact the per-row action column.",
        "exportInventory": "Export the current (optionally filtered) inventory from the page-header table-tools cluster — CSV / Excel / PDF.",
        "stockIn": "Record incoming stock for the selected item from its row action.",
        "stockOut": "Record outgoing stock for the selected item from its row action.",
        "editItem": "Edit the selected inventory item (e.g. min-stock threshold, supplier) from its row action.",
        "importFatturapa": "Open the FatturaPA XML import dialog, upload supplier e-invoice XML files, review the classified line preview, deselect any unwanted lines, and confirm to create articles and stock movements."
      }
    },
    "workflowWebhookAction": {
      "workflows": {
        "readStep": "Read the webhook step and what is wrong with it. Each field sits in a wrapper with `data-field` (`url`, `secret`, `legalBasisConfirmed`, `parameters`). A field with a problem has `aria-invalid=\"true\"` on its control and its message in a `data-slot=\"error\"` element inside the wrapper. Parameter rows carry `data-parameter-id`, an opaque row id. The `secret` wrapper is write-only for agents: read whether it has a problem, never its value, and never repeat a secret.",
        "generateSecret": "Generate a random signing secret into the password field (`data-action=\"generate-secret\"`; again to replace it) and copy it to the clipboard (`data-action=\"copy-secret\"`, which shows `data-copied=\"true\"` once done) so the person can paste it into the receiving app. The secret never appears in the page markup. Nothing is saved until the host form is submitted.",
        "editParameters": "Open the Advanced section (`data-action=\"toggle-advanced\"`) and add, change or remove extra parameters, each a name and a value. Most receiving apps, Link included, need none. A value may contain variables, inserted from each row's \"Insert variable\" menu or copied from the list under the rows (`data-action=\"copy-field\"`); the server replaces them with the patient's details when it sends.",
        "sendTest": "Send one sample message to the address (`data-action=\"send-test\"`, present only when the host supports it): sample IDs, no patient data, signed with the secret. The address and secret are checked first. The answer is in `data-slot=\"test-result\"`, with `data-test-ok=\"true\"` when the app answered 2xx.",
        "readMessageShape": "Open \"What Alfadocs sends\" (`data-action=\"toggle-reference\"`) to read what every message carries: an example body and example headers, each in a code block with its own copy button, then each field and header with its meaning. `data-action=\"copy-field\"` copies one field name and shows `data-copied=\"true\"` once done. The example values are invented; the shape is the platform's.",
        "saveStep": "Save through the host's own form and button. The fields stop the submit while the address, the secret, the consent box or a parameter name has a problem, and move focus to the first one. The person ticks the consent box themselves: it confirms the practice has a legal basis to share patient data."
      }
    }
  }
}
