{{/* chat-panel — the suite's first chat panel (spec §6, §10): one self-contained partial whose only companions are static/chat.js and static/chat.css. It names nothing about Calls — no sqid, no room, no room.js — so a second app adopts all three files unchanged: parse this file, drop {{template "chat-panel" .}} where the panel belongs, load the two assets, and call Chat.mount() with a send function. It takes no data. Everything it shows arrives through the mount API at runtime, because chat is never stored and never replayed (§6): the log starts empty on every load and goes with the tab. hidden is not decoration. The whole panel is script-only — a Send button with no script behind it is a lie — so the server renders it hidden and the script's host un-hides it when the panel is usable. chat.css gives .chat a display of its own, so it carries the matching .chat[hidden] rule; see the note at the top of that file. The control is the vendored field envelope written out rather than called: ui's field-textarea has no Maxlength key, and §4.4's 2 KB is the browser's own courtesy cap on a control the server will refuse above it. Same attributes, same wiring — label bound with for=, the status line named in aria-describedby so the two sentences the panel can say reach a screen reader as part of the field and not only as a live region. No `required`, deliberately. This form never reaches a server, so the attribute would buy exactly one thing: the browser's own validation bubble on an empty click of Send — a sentence in wording and tone nobody here chose, and one this app's copy list could not catalogue. Enter cannot raise that bubble anyway without routing through requestSubmit(), so keeping it would leave the two ways of sending behaving differently on an empty box. Without it both do the same nothing, which is what every chat box does. */}} {{define "chat-panel"}}{{end}}