
Live Artifact
- 129 installs
- 83.7k repo stars
- Updated August 5, 2026
- nexu-io/open-design
Spin up a live HTML design studio where each page section can be swapped in place so stakeholders pick variants before you lock tokens for production.
About
live-artifact from nexu-io/open-design produces a self-contained interactive design studio page. Teams iterate on nav, hero, lists, and footer variants with theme pills and palettes, then port the chosen look into production code.
- In-place component swapping
- Global theme controls
- Mix-and-match layouts
- Pre-production token lock-in
Live Artifact by the numbers
- 129 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,047 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nexu-io/open-design --skill live-artifactAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 129 |
|---|---|
| repo stars | ★ 83.7k |
| Last updated | August 5, 2026 |
| Repository | nexu-io/open-design ↗ |
What it does
Spin up a live HTML design studio where each page section can be swapped in place so stakeholders pick variants before you lock tokens for production.
Files
Live Artifact Skill
Create an Open Design live artifact: a project-scoped, previewable HTML artifact whose data can later be refreshed without redesigning the presentation.
Resource map
live-artifact/
├── SKILL.md
└── references/
├── artifact-schema.md ← `references/artifact-schema.md`: artifact files, DTO shape, template binding rules
├── connector-policy.md ← `references/connector-policy.md`: connector safety, redaction, credential boundaries
└── refresh-contract.md ← `references/refresh-contract.md`: source metadata, refresh execution, snapshotsCurrent status
Use the references in this directory as the source of truth for the live artifact file contract. Prefer daemon wrapper commands over raw HTTP when registering or updating live artifacts.
When to use this skill
Use this skill when the user asks for a data-backed view that should remain useful after the first render, for example a live dashboard, refreshable report, synced status page, auditable data view, or artifact that can later be refreshed from local/project data or connectors.
Before creating files, decide whether the user actually wants a live artifact or a normal static artifact:
- Use a live artifact when the user mentions refresh, sync, recurring updates, connector-backed data, source/provenance tracking, dashboards, reports, or reusable data-backed views.
- Use a normal static artifact when the user only wants a one-off HTML/mockup/image/file and does not need refresh, source metadata, or data/provenance panels.
- If the intent is ambiguous, ask one short question: “Should this be refreshable/live, or just a static artifact?”
Workflow
1. Resolve scope and data source without blocking on connected connectors
- Identify the preview goal, audience, data freshness expectations, and whether refresh should be possible later.
- If the user explicitly names a connector/source such as Notion, GitHub, Slack, or Google Drive, do not ask “where should the data come from?” before checking daemon connector tools.
- Prefer local/project sources or daemon connector tools when available.
- Do not call provider APIs directly when a daemon connector/wrapper exists.
- If connector data is needed, first list connectors with
"$OD_NODE_BIN" "$OD_BIN" tools connectors list --format compact. If the named connector is present withstatus: "connected", choose an appropriate read-onlyautotool from its catalog and execute it through the connector wrapper. - For Notion specifically, a connected
notionconnector plus a user brief that names Notion is enough to start withnotion.notion_searchusing a query derived from the requested artifact/topic. Usenotion.notion_fetch_databaseonly when the user supplied a database id or the search result clearly identifies one. - Ask the user a data-source question only when no matching connected connector exists, multiple connected candidates fit equally well, or the requested artifact has no usable topic/query to search for. If you must ask, be specific: ask for the page/database/topic or permission to search broadly, not “where is the Notion data source?”
2. Author the source files
- Write
template.htmlas the human-designed HTML template. - Write
data.jsonas the canonical preview data used by{{data.path}}bindings. - Write
artifact.jsonwith the live artifact metadata, preview declaration, document declaration, and safe source descriptors. - Write
provenance.jsonwith concise source notes, timestamps, non-sensitive connector references, and transformation notes. - Do not author
index.htmlas source. The daemon derivesindex.htmlfromtemplate.htmlanddata.json.
3. Keep data compact and preview-oriented
- Store only normalized values needed by the preview.
- Summarize large lists, provider responses, or logs before writing them into
data.json. - Stay within the bounded JSON rules in
references/artifact-schema.md.
4. Apply safety rules before registration
- Never store credentials, OAuth tokens, API keys, cookies, auth headers, raw provider responses, HTTP envelopes, full payloads, or secret-like fields in
artifact.json,data.json,provenance.json, or source metadata. - Avoid forbidden key names such as
raw,rawResponse,payload,body,headers,cookie,authorization,token,secret,credential, andpasswordanywhere in persisted JSON. - Use escaped
html_template_v1interpolation only. Raw/unescaped HTML interpolation is not allowed.
5. Register or update through daemon wrappers
- Use the Open Design daemon wrapper commands via
"$OD_NODE_BIN" "$OD_BIN"instead of rawcurl, barenode, or bareod:
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts list --format compact
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts update --artifact-id "$ARTIFACT_ID" --input artifact.json- The wrapper reads injected
OD_NODE_BIN,OD_BIN,OD_DAEMON_URL, andOD_TOOL_TOKEN; do not print, persist, or override token values. - Do not include or invent
projectId; the daemon derives project/run scope from the token. - Use raw HTTP only for daemon development/debugging when explicitly requested.
6. Use connector wrappers for connector data
- Discover available connectors and tools:
"$OD_NODE_BIN" "$OD_BIN" tools connectors list --format compact- Execute a read-only connector tool with a JSON object input file:
"$OD_NODE_BIN" "$OD_BIN" tools connectors execute --connector "$CONNECTOR_ID" --tool "$TOOL_NAME" --input input.json- Persist only the compact normalized fields needed by the preview plus non-sensitive connector references (
connectorId,toolName,accountLabel). Never persist connector credentials, transport metadata, or raw provider output. - Do not ask for connector secrets or duplicate setup. If
statusisconnected, use the listed tools; if it is not connected, tell the user to connect it in the UI. - See
references/connector-policy.mdfor listing/execution and credential boundaries, andreferences/refresh-contract.mdfor read-only refresh source metadata.
7. Report concise results
- On success, return the artifact ID/title and note that
index.htmlis daemon-derived. - On validation failure, fix the source files and retry through the wrapper. Do not bypass validation.
Required files
Every live artifact creation flow must produce these source files before registration:
template.html— declared skill output and source template for the preview.data.json— compact, canonical preview data.artifact.json— create/update input for daemon validation.provenance.json— safe source and transformation summary.
index.html is the primary preview entry declared in frontmatter, but it is derived daemon output rather than agent-authored source.
{
"brand_name": "ST. LUKES",
"greeting": "Hey Lukmon, glad to have you back! 🙌",
"search_placeholder": "Search doctors, patients, rooms…",
"search_shortcut": "⌘K",
"secondary_action_label": "Export CSV",
"primary_action_label": "Add new",
"user": {
"name": "Lukmon Olabode",
"role": "Admin",
"av_class": "av-orange",
"initial": "L"
},
"nav_main_label": "Main Menu",
"nav_main": [
{ "label": "Dashboard", "active_class": "active", "count": "" },
{ "label": "Message", "active_class": "", "count": "10" },
{ "label": "Schedule", "active_class": "", "count": "" },
{ "label": "Notification", "active_class": "", "count": "12" },
{ "label": "Transaction", "active_class": "", "count": "" }
],
"nav_management_label": "Management",
"nav_management": [
{ "label": "Doctor", "active_class": "", "count": "" },
{ "label": "Medicine", "active_class": "", "count": "" },
{ "label": "Bedroom", "active_class": "", "count": "" },
{ "label": "Appointment", "active_class": "", "count": "" },
{ "label": "Patient", "active_class": "", "count": "" }
],
"pro_card": {
"tag": "Pro",
"title": "Pssst!",
"body": "Your subscription expires in 9 days.",
"primary_label": "Renew",
"secondary_label": "Cancel"
},
"kpi_a": {
"label": "Total doctors",
"value": "1,089",
"trend_class": "down",
"trend_label": "↓ 4.2%",
"caption": "An increase of 20 doctors in the last 7 days.",
"strip_class": "stripe-amber"
},
"kpi_b": {
"label": "Total bookings",
"value": "17,610",
"trend_class": "up",
"trend_label": "↑ 5.5%",
"caption": "Last 7 days: 5,231 → 8,323 visitors.",
"strip_class": "stripe-blue",
"mini_stat": "1,635 today"
},
"kpi_c": {
"label": "Available rooms",
"value": "8,450",
"trend_class": "up",
"trend_label": "↑ 462",
"rows": [
{ "label": "General room", "value": "100" },
{ "label": "Private room", "value": "75" }
]
},
"kpi_d": {
"label": "Total visitors",
"value": "29,709",
"trend_class": "up",
"trend_label": "↑ 3.5%",
"caption": "Top 3 in-demand clinics this month.",
"strip_class": "stripe-amber",
"mini_stat": "1,070 today"
},
"chart": {
"title": "Patient overview",
"dropdown_label": "Last 6 months",
"legend_a": "Total patients",
"legend_b": "Avg. hospitalized",
"legend_c": "Avg. outpatient care",
"bars": [
{ "x": "34", "y": "148", "h": "92" },
{ "x": "58", "y": "108", "h": "132" },
{ "x": "120", "y": "124", "h": "116" },
{ "x": "144", "y": "92", "h": "148" },
{ "x": "206", "y": "96", "h": "144" },
{ "x": "230", "y": "56", "h": "184" },
{ "x": "292", "y": "156", "h": "84" },
{ "x": "316", "y": "116", "h": "124" },
{ "x": "378", "y": "140", "h": "100" },
{ "x": "402", "y": "100", "h": "140" },
{ "x": "464", "y": "120", "h": "120" },
{ "x": "488", "y": "80", "h": "160" },
{ "x": "550", "y": "160", "h": "80" },
{ "x": "574", "y": "128", "h": "112" }
],
"x_labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]
},
"calendar": {
"month_label": "March 2025",
"dow": ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"],
"days": [
{ "label": "23", "modifier": "muted" }, { "label": "24", "modifier": "muted" }, { "label": "25", "modifier": "muted" }, { "label": "26", "modifier": "muted" }, { "label": "27", "modifier": "muted" }, { "label": "28", "modifier": "muted" }, { "label": "1", "modifier": "" },
{ "label": "2", "modifier": "" }, { "label": "3", "modifier": "" }, { "label": "4", "modifier": "" }, { "label": "5", "modifier": "" }, { "label": "6", "modifier": "" }, { "label": "7", "modifier": "" }, { "label": "8", "modifier": "active" },
{ "label": "9", "modifier": "" }, { "label": "10", "modifier": "" }, { "label": "11", "modifier": "" }, { "label": "12", "modifier": "" }, { "label": "13", "modifier": "" }, { "label": "14", "modifier": "" }, { "label": "15", "modifier": "" },
{ "label": "16", "modifier": "" }, { "label": "17", "modifier": "" }, { "label": "18", "modifier": "" }, { "label": "19", "modifier": "" }, { "label": "20", "modifier": "" }, { "label": "21", "modifier": "" }, { "label": "22", "modifier": "" },
{ "label": "23", "modifier": "" }, { "label": "24", "modifier": "" }, { "label": "25", "modifier": "" }, { "label": "26", "modifier": "" }, { "label": "27", "modifier": "" }, { "label": "28", "modifier": "" }, { "label": "29", "modifier": "" }
]
},
"activity": {
"title": "Activity Detail · Mar 8",
"events": [
{ "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" },
{ "av_class": "pink", "name": "Dental staff meetup", "time": "3:00 pm" },
{ "av_class": "violet", "name": "Ola Muhammad intake", "time": "4:00 pm" }
],
"add_label": "+ Add item"
},
"donut": {
"title": "Top 3 most requested clinics",
"center_label": "Total patients",
"center_num": "534",
"segment_b": { "dasharray": "120 240", "dashoffset": "0" },
"segment_c": { "dasharray": "35 240", "dashoffset": "-120" },
"legend": [
{ "value": "120", "label": "Dental" },
{ "value": "249", "label": "Cardiology" },
{ "value": "165", "label": "Surgery" }
]
},
"schedule": {
"title": "Doctors's schedule",
"stats": [
{ "value": "51", "small": "Total", "label": "Available" },
{ "value": "23", "small": "Total", "label": "Unavailable" },
{ "value": "09", "small": "Total", "label": "Leave" }
],
"list_header_label": "List of Doctor",
"doctors": [
{ "av_class": "av-blue", "initial": "P", "name": "Peter Bashir", "role": "Anesthesiologist", "status_class": "avail", "status_label": "Available" },
{ "av_class": "av-violet", "initial": "D", "name": "Deborah Fagbemi", "role": "Cardiologist", "status_class": "unav", "status_label": "Unavailable" },
{ "av_class": "av-rose", "initial": "H", "name": "Hannah Diongoli", "role": "Dermatologist", "status_class": "avail", "status_label": "Available" },
{ "av_class": "av-amber", "initial": "A", "name": "Aisha Bello", "role": "Pediatrician", "status_class": "leave", "status_label": "Leave" }
]
},
"appointments": {
"title": "Today's appointments",
"list": [
{ "av_class": "av-pink", "initial": "R", "name": "Ruth Tubonimi", "role": "Gastroenterology · room 204", "date": "Today", "time": "09:40" },
{ "av_class": "av-amber", "initial": "J", "name": "Joseph Obiano", "role": "Psychiatry · video call", "date": "Today", "time": "10:25" },
{ "av_class": "av-mint", "initial": "T", "name": "Timothy Jibrin", "role": "Hematology · room 117", "date": "Today", "time": "11:00" },
{ "av_class": "av-violet", "initial": "E", "name": "Elizabeth Kanu", "role": "Ophthalmology · room 09", "date": "Today", "time": "02:15" },
{ "av_class": "av-blue", "initial": "S", "name": "Simon Garba", "role": "Otolaryngology · room 21", "date": "Today", "time": "03:40" }
]
}
}
Clinic Console — live artifact template
A html_template_v1 template for a friendly clinic / hospital / telemedicine operations console. Soft-mint healthcare aesthetic: cool off-white canvas, single mint accent, generous 18px card radii, signature diagonal-stripe pattern fills inside KPI tiles and bar-chart bars, illustrated CSS-gradient avatars, and one dark surface (the calendar activity popover).
Files
clinic-console/
├── template.html # html_template_v1 source — only DOM, CSS tokens, and {{data.*}} bindings
├── data.json # canonical default sample (renders straight out of the box)
└── README.md # this file — data contract + customization notesHow an agent uses this template
This template is intended to be copied (or referenced) when the `live-artifact` skill is invoked with a healthcare operations brief such as "clinic dashboard", "doctors schedule", "hospital admin", "appointment console", "telemedicine ops", "诊所后台", "医院管理".
The agent should:
1. Copy template.html and data.json into the project's live-artifact workspace directory as template.html and data.json. 2. Edit data.json to reflect the actual brand, names, schedules, and numbers from the brief or connector source. The shape of the JSON must match what template.html references (every {{data.path}} interpolation below). 3. Author artifact.json and provenance.json per the live-artifact protocol, then register the artifact through the daemon wrapper:
"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts create --input artifact.json4. The daemon renders template.html + data.json into the preview index.html automatically. The agent does not author index.html.
When the user clicks Refresh, the daemon re-runs the registered source, maps results back into data.json, re-renders the preview, and snapshots the change — the layout never changes; only the numbers, names, and pill states do.
Default sample renders out of the box
If you create a live artifact using the default data.json shipped here, you get the canonical "St. Lukes Wellness" demo screen:
- Greeting:
Hey Lukmon, glad to have you back! 🙌 - Four KPI tiles: Total doctors / Total bookings / Available rooms / Total
visitors, with mixed amber- and blue-stripe pattern footers and an inline General/Private rooms mini-list.
- Patient overview chart with paired diagonal-stripe bars across Jan–Jul,
Mar 2025 highlighted with a mint outline.
- March 2025 mini-calendar with day 8 active (mint circle + dot) and a dark
Activity Detail popover floating below it.
- Top requested clinics donut: Dental 120 / Cardiology 249 / Surgery 165.
- Doctor schedule with three pastel pills (Available / Unavailable / Leave).
- Today's appointments list with five illustrated avatars + venue / mode
hints (room 204, video call, …).
Default sample provenance.json
If you ship the default sample without re-sourcing the data, use:
{
"generatedAt": "2026-04-29T12:00:00.000Z",
"generatedBy": "agent",
"notes": "Default sample data shipped with the clinic-console template. Replace with real clinic data before sharing externally.",
"sources": [
{ "label": "Template default sample", "type": "user_input" }
]
}Data contract
The shape below is the contract between template.html and data.json. Every key listed is referenced by at least one {{data.path}} interpolation in template.html. All values are scalars (string or number); the template does not invoke any expression / helper / conditional logic — it is a straight html_template_v1 substitution.
Top-level scalars
| Key | Example | Notes |
|---|---|---|
brand_name | "ST. LUKES" | Sidebar wordmark. Keep ≤14 characters. |
greeting | "Hey Lukmon, glad to have you back! 🙌" | Single emoji allowed at the end; no other emoji anywhere in the artifact. |
search_placeholder | "Search doctors, patients, rooms…" | Greeting-row search input ghost text. |
search_shortcut | "⌘K" | Right-side keycap label. |
secondary_action_label | "Export CSV" | Greeting-row secondary button text. |
primary_action_label | "Add new" | Greeting-row primary mint CTA text. |
user
| Key | Example | Notes |
|---|---|---|
name | "Lukmon Olabode" | Sidebar bottom row. |
role | "Admin" | One-word role; longer roles wrap. |
av_class | "av-orange" | One of av-orange, av-pink, av-mint, av-blue, av-violet, av-amber, av-rose. |
initial | "L" | Single uppercase letter. |
nav_main and nav_management (5 items each)
Each item shape:
| Key | Example | Notes |
|---|---|---|
label | "Dashboard" | Nav text. |
active_class | "" or "active" | Set to "active" on exactly one nav item across both groups. |
count | "" or "10" | Empty string hides the count badge (CSS :empty { display: none }). |
Icons are template-locked. Each nav slot's icon is hardcoded inside
template.html (see Icons are template-lockedbelow) and is not exposed throughdata.json. Thehtml_template_v1
security validator forbids {{data.*}} interpolation inside URL-bearingattributes (<use href>,<a href>,<img src>, …) — and even if it
didn't, the validator runs before substitution, so a malformed data.jsoncould smuggle a javascript: URL past it. The reorder rule is therefore:if you change the meaning of a nav slot, also edit the corresponding
<use href="#icon-…">literal intemplate.html.
pro_card
| Key | Example | Notes |
|---|---|---|
tag | "Pro" | Black pill in the upgrade card. Keep ≤6 characters. |
title | "Pssst!" | Display title. |
body | "Your subscription expires in 9 days." | One-sentence nudge. |
primary_label | "Renew" | Mint primary action. |
secondary_label | "Cancel" | Outlined secondary action. |
KPI tiles kpi_a kpi_b kpi_c kpi_d
Tiles A, B, D share the caption + pattern strip layout. Tile C uses a 2-row mini-list layout instead. Every tile must have either a strip or a mini-list — never bare.
Common keys:
| Key | Example | Notes |
|---|---|---|
label | "Total doctors" | Tile label. |
value | "1,089" | Big number (Plus Jakarta Sans 700). Use commas for thousands. |
trend_class | "up" or "down" | Pill grammar — up = mint, down = rose. |
trend_label | "↑ 5.5%" | Always include the arrow glyph. |
KPI icons are also template-locked — see
Icons are template-locked below.
A / B / D additional keys:
| Key | Example | Notes |
|---|---|---|
caption | "An increase of 20 doctors in the last 7 days." | One sentence answering "compared to what". |
strip_class | "stripe-amber" | One of stripe-amber, stripe-blue, stripe-mint. Adjacent tiles should alternate hues. |
mini_stat (B / D only) | "1,635 today" | Right-aligned tiny caption below the strip. |
C (kpi_c) additional keys:
| Key | Example |
|---|---|
rows | array of 2 objects: { "label": "General room", "value": "100" } |
chart
| Key | Example | Notes |
|---|---|---|
title | "Patient overview" | Card title. |
dropdown_label | "Last 6 months" | Time-range chip text. |
legend_a legend_b legend_c | "Total patients" etc. | Three legend captions. |
bars | array of 14 objects: { "x": "34", "y": "148", "h": "92" } | 7 month pairs (mint back, blue front). Bar 5 (index 5) is the highlighted month — the template adds a 2px mint stroke to bar 5 only. |
x_labels | ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"] | Seven month labels matching the seven bar pairs. |
calendar
| Key | Example | Notes |
|---|---|---|
month_label | "March 2025" | Header. |
dow | ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"] | Always 7 items. |
days | array of exactly 35 objects: { "label": "1", "modifier": "" } | 5 weeks × 7 days. modifier = "", "muted" (leading/trailing month), or "active" (single highlighted day, mint circle). |
activity
| Key | Example | Notes |
|---|---|---|
title | "Activity Detail · Mar 8" | Popover header — should reference the active calendar day. |
events | array of exactly 3 objects: { "av_class": "blue", "name": "Dr. Sarah · post-op review", "time": "11:00 am" } | Three events. av_class ∈ blue, pink, violet, mint, amber. |
add_label | "+ Add item" | Footer link. |
donut
| Key | Example | Notes |
|---|---|---|
title | "Top 3 most requested clinics" | Card title. |
center_label | "Total patients" | Above the center number. |
center_num | "534" | The big tabular number in the donut hole. |
segment_b | { "dasharray": "120 240", "dashoffset": "0" } | SVG stroke-dasharray + stroke-dashoffset for the pink slice. Sum of arc lengths over a r=38 circle equals 2π × 38 ≈ 239. |
segment_c | { "dasharray": "35 240", "dashoffset": "-120" } | Same for the mint slice. The blue background ring is the full circumference — no per-segment math needed. |
legend | array of exactly 3 objects: { "value": "120", "label": "Dental" } | Three entries in blue / pink / mint order. |
schedule
| Key | Example | Notes |
|---|---|---|
title | "Doctors's schedule" | Card title. |
stats | array of exactly 3 objects: { "value": "51", "small": "Total", "label": "Available" } | Available / Unavailable / Leave counts in a 3-column grid. |
list_header_label | "List of Doctor" | Sortable list header label. |
doctors | array of exactly 4 objects | See below. |
Each doctors row:
| Key | Example | Notes |
|---|---|---|
av_class | "av-blue" | Avatar gradient. |
initial | "P" | Single uppercase letter. |
name | "Peter Bashir" | Real-feeling name. |
role | "Anesthesiologist" | Specialty. |
status_class | "avail" "unav" "leave" | Pill grammar. |
status_label | "Available" "Unavailable" "Leave" | Pill text. |
appointments
| Key | Example |
|---|---|
title | "Today's appointments" |
list | array of exactly 5 objects |
Each list row:
| Key | Example | Notes |
|---|---|---|
av_class | "av-pink" | Avatar gradient. |
initial | "R" | Single uppercase letter. |
name | "Ruth Tubonimi" | Real-feeling name. |
role | "Gastroenterology · room 204" | Specialty + venue / mode hint (room N, video call, telemedicine). |
date | "Today" | Short date label. |
time | "09:40" | 24h or 12h, pick one and stay consistent. |
Icons are template-locked
html_template_v1 forbids {{data.*}} interpolation inside URL-bearing attributes such as <use href>, <a href>, <img src>, <form action>, etc. (see `skills/live-artifact/references/artifact-schema.md`). The renderer's security validator runs before {{data.*}} substitution, so even a well-formed validator pass would not protect a future data.json that put javascript:alert(1) (or any other URL value) into one of these attributes.
This template therefore hardcodes every <use href="#icon-…"> reference in template.html itself. Each slot has a fixed icon id:
| Slot | Hardcoded icon id |
|---|---|
| Sidebar brand mark | #icon-leaf |
| Sidebar collapse toggle | #icon-collapse |
nav_main[0] Dashboard | #icon-dashboard |
nav_main[1] Message | #icon-message |
nav_main[2] Schedule | #icon-schedule |
nav_main[3] Notification | #icon-bell |
nav_main[4] Transaction | #icon-card |
nav_management[0] Doctor | #icon-user |
nav_management[1] Medicine | #icon-pill |
nav_management[2] Bedroom | #icon-bed |
nav_management[3] Appointment | #icon-check-square |
nav_management[4] Patient | #icon-people |
| Sidebar logout | #icon-logout |
| Greeting-row search | #icon-search |
| Greeting-row secondary CTA | #icon-download |
| Greeting-row primary CTA | #icon-plus |
kpi_a glyph | #icon-user |
kpi_b glyph | #icon-schedule |
kpi_c glyph | #icon-bed |
kpi_d glyph | #icon-people |
| Patient-overview card | #icon-clock |
| Time-range dropdown chevron | #icon-chev-down |
| Calendar prev / next | #icon-chev-left, #icon-chev-right |
| Top-clinics card | #icon-stethoscope |
| Doctor-schedule card | #icon-schedule |
| List header chevron | #icon-chev-down |
| Today's-appointments card | #icon-check-square |
If you re-purpose a slot (e.g. swap nav_main[2] Schedule for nav_main[2] Reports), edit the corresponding <use href="#icon-…"> literal in template.html to match — the icon set inside the inline <symbol> defs at the top of template.html already includes 21 icons covering the common clinic / hospital / pharmacy / telemedicine vocabulary (#icon-dashboard, #icon-message, #icon-schedule, #icon-bell, #icon-card, #icon-user, #icon-pill, #icon-bed, #icon-check-square, #icon-people, #icon-leaf, #icon-clock, #icon-stethoscope, #icon-search, #icon-download, #icon-plus, #icon-chev-left, #icon-chev-right, #icon-chev-down, #icon-collapse, #icon-logout).
If you need a runtime-configurable icon, add a new constrained, non-URL-bearing mechanism (for example a data.kpi_a.icon_class that toggles between a fixed list of CSS classes the template enumerates) — never interpolate into <use href> directly.
Style guarantees
The template enforces, in CSS only (no JavaScript):
- Cool off-white canvas (
#EEF2F6), bright white surfaces, 18px card radii, 1px hairline borders. - Mint accent (
#10B981) restricted to five places: active sidebar nav row, primary CTA, KPI icon glyphs, success metric pill, active calendar date. - Diagonal-stripe pattern fills (135°, 8px line + 8px gap) on KPI footer strips and inside bar-chart bars.
- Pastel-only status pills (mint / rose / amber).
- Tabular lining numerals on every numeric value (
font-feature-settings: "tnum","lnum"). - The dark calendar activity popover is the only dark surface in the artifact.
- Mobile reflow at ≤920px: sidebar stacks above main, KPI strip becomes 2 cols then 1 col, mid and bottom rows stack.
- No external CDN imports. Fonts use system fallback (
Plus Jakarta Sans, Inter, system-ui, sans-serif).
Customization tips
- Telemedicine variant: replace
kpi_c(Available rooms) withLive sessions, swap the donut toTop consultation types(Video / Audio / Chat), and add· video call/· audio callvenue hints in appointment rows. - Pharmacy variant: replace the doctor schedule with stock levels — keep the same shape, just rename the columns to SKU / drug / stock pill.
- Pediatric variant: tilt the avatar palette toward
av-pink,av-amber,av-orange, keep the active calendar day on a children's milestone.
For all variants, do not introduce new colors, fonts, or radii. Every visual lever is already a token in :root{}.
Bounded JSON envelope
This default data.json is well within the live-artifact bounded JSON constraints:
| Constraint | Limit | This sample |
|---|---|---|
| Object/array depth | 8 | 4 |
| Object keys | 100 / object | ≤20 |
| Array length | 500 | 35 (calendar.days) |
| String length | 16 KiB | <100 chars |
| Serialized size | 256 KiB | ~7 KiB |
If you scale up the bar count, calendar density, or list rows, stay well under these limits. Refresh writes go through the same validation, so oversized data will be rejected before persistence.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{data.brand_name}} — Clinic Operations Console</title>
<style>
:root {
--canvas: #eef2f6;
--surface: #ffffff;
--surface-muted: #f8fafc;
--surface-inverse: #0f172a;
--border: #e5e7eb;
--border-strong: #d6dbe3;
--fg: #0f172a;
--fg-muted: #64748b;
--fg-tertiary: #94a3b8;
--fg-on-inverse: #f8fafc;
--accent: #10b981;
--accent-strong: #059669;
--accent-soft: #d1fae5;
--pill-up-bg: #d1fae5; --pill-up-fg: #059669;
--pill-down-bg: #fee2e2; --pill-down-fg: #dc2626;
--pill-avail-bg: #d1fae5; --pill-avail-fg: #047857;
--pill-unav-bg: #fee2e2; --pill-unav-fg: #b91c1c;
--pill-leave-bg: #fef3c7; --pill-leave-fg: #b45309;
--stripe-blue: #bfdbfe; --stripe-amber: #fde68a; --stripe-mint: #a7f3d0;
--donut-blue: #bfdbfe; --donut-pink: #fbcfe8; --donut-mint: #ccfbf1;
--legend-blue: #3b82f6; --legend-amber: #f59e0b; --legend-pink: #ec4899; --legend-mint: #2dd4bf;
--av-orange-1: #fed7aa; --av-orange-2: #f97316; --av-orange-fg: #7c2d12;
--av-pink-1: #fbcfe8; --av-pink-2: #ec4899;
--av-mint-1: #a7f3d0; --av-mint-2: #10b981;
--av-blue-1: #bfdbfe; --av-blue-2: #3b82f6;
--av-violet-1: #ddd6fe; --av-violet-2: #7c3aed;
--av-amber-1: #fde68a; --av-amber-2: #f59e0b; --av-amber-fg: #78350f;
--av-rose-1: #fecaca; --av-rose-2: #ef4444;
--av-fg-light: #ffffff;
--r-card: 18px; --r-pill: 999px; --r-md: 14px; --r-sm: 10px; --r-xs: 8px;
--shadow-card: 0 1px 2px rgba(15,23,42,.04), 0 2px 6px rgba(15,23,42,.04);
--shadow-popover: 0 12px 28px rgba(2,6,23,.32);
--font-display: 'Plus Jakarta Sans', 'SF Pro Display', system-ui, sans-serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 20px;
background: var(--canvas); color: var(--fg);
font: 14px/1.5 var(--font-body);
font-feature-settings: "tnum", "lnum";
display: grid; grid-template-columns: 240px 1fr; gap: 20px;
min-height: 100vh;
}
@media (max-width: 920px) {
body { grid-template-columns: 1fr; }
aside.sidebar { position: static !important; height: auto !important; }
}
.card, aside.sidebar {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--r-card); box-shadow: var(--shadow-card);
}
aside.sidebar {
padding: 18px; display: flex; flex-direction: column; gap: 16px;
position: sticky; top: 20px; height: calc(100vh - 40px);
}
.brand { display: flex; align-items: center; justify-content: space-between; padding: 0 6px; }
.brand-mark { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-weight: 700; font-size: 17px; letter-spacing: -0.01em; }
.brand-leaf { width: 22px; height: 22px; border-radius: var(--r-pill); background: var(--accent); color: var(--surface); display: inline-flex; align-items: center; justify-content: center; }
.brand-toggle { color: var(--fg-tertiary); }
.nav-section { display: flex; flex-direction: column; gap: 2px; }
.nav-label { color: var(--fg-tertiary); font-size: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; padding: 8px 10px 4px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-sm); color: var(--fg-muted); font-size: 13px; }
.nav-item .ico { color: var(--fg-muted); }
.nav-item .count { margin-left: auto; background: var(--surface-muted); color: var(--fg-muted); border-radius: var(--r-pill); padding: 1px 8px; font-size: 11px; font-weight: 500; }
.nav-item .count:empty { display: none; }
.nav-item.active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; }
.nav-item.active .ico { color: var(--accent-strong); }
.pro-card { margin-top: auto; background: var(--surface-muted); border-radius: var(--r-md); padding: 14px; display: flex; flex-direction: column; gap: 8px; }
.pro-tag { display: inline-flex; background: var(--fg); color: var(--surface); border-radius: var(--r-pill); padding: 2px 8px; font-size: 10px; font-weight: 600; width: fit-content; }
.pro-card h4 { margin: 0; font-size: 13px; font-weight: 600; font-family: var(--font-display); }
.pro-card p { margin: 0; font-size: 11px; color: var(--fg-muted); }
.pro-actions { display: flex; gap: 8px; margin-top: 4px; }
.pro-actions button { font-size: 11px; padding: 6px 12px; border-radius: var(--r-sm); border: 1px solid var(--border); background: var(--surface); color: var(--fg-muted); cursor: pointer; }
.pro-actions .primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
.user-row { display: flex; align-items: center; gap: 10px; padding: 10px 6px 0; border-top: 1px solid var(--border); }
.user-row .meta { display: flex; flex-direction: column; }
.user-row .name { font-size: 13px; font-weight: 600; }
.user-row .role { font-size: 11px; color: var(--fg-tertiary); }
.user-row .out { margin-left: auto; color: var(--fg-tertiary); }
.av { border-radius: var(--r-pill); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--av-fg-light); font-weight: 600; }
.av-32 { width: 32px; height: 32px; font-size: 12px; }
.av-40 { width: 40px; height: 40px; font-size: 14px; }
.av-orange { background: linear-gradient(135deg, var(--av-orange-1), var(--av-orange-2)); color: var(--av-orange-fg); }
.av-pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
.av-mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
.av-blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
.av-violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
.av-amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); color: var(--av-amber-fg); }
.av-rose { background: linear-gradient(135deg, var(--av-rose-1), var(--av-rose-2)); }
main { display: flex; flex-direction: column; gap: 24px; }
.greeting-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.greeting { font-family: var(--font-display); font-size: 26px; font-weight: 700; letter-spacing: -0.01em; }
.actions { display: flex; align-items: center; gap: 12px; }
.search { display: flex; align-items: center; gap: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm); padding: 9px 14px; width: 260px; color: var(--fg-tertiary); font-size: 13px; }
.search .kbd { margin-left: auto; font-size: 11px; border: 1px solid var(--border); padding: 1px 6px; border-radius: 4px; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; border-radius: var(--r-sm); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-family: var(--font-body); }
.btn.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
.kpi-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
@media (max-width: 1100px) { .kpi-strip { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .kpi-strip { grid-template-columns: 1fr; } }
.kpi { padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 150px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-card); box-shadow: var(--shadow-card); }
.kpi-head { display: flex; align-items: center; gap: 10px; }
.kpi-glyph { width: 32px; height: 32px; border-radius: var(--r-sm); background: var(--accent-soft); color: var(--accent-strong); display: inline-flex; align-items: center; justify-content: center; }
.kpi-label { font-size: 13px; color: var(--fg-muted); font-weight: 500; flex: 1; }
.kpi-menu { color: var(--fg-tertiary); }
.kpi-num-row { display: flex; align-items: baseline; gap: 10px; }
.kpi-num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1.1; letter-spacing: -0.015em; }
.pill { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--r-pill); padding: 2px 8px; font-size: 11px; font-weight: 600; }
.pill.up { background: var(--pill-up-bg); color: var(--pill-up-fg); }
.pill.down { background: var(--pill-down-bg); color: var(--pill-down-fg); }
.pill.avail { background: var(--pill-avail-bg); color: var(--pill-avail-fg); font-weight: 500; }
.pill.unav { background: var(--pill-unav-bg); color: var(--pill-unav-fg); font-weight: 500; }
.pill.leave { background: var(--pill-leave-bg); color: var(--pill-leave-fg); font-weight: 500; }
.kpi-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; }
.kpi-cap { font-size: 12px; color: var(--fg-tertiary); line-height: 1.4; max-width: 60%; }
.kpi-strip-pat { height: 32px; flex-shrink: 0; border-radius: var(--r-xs); }
.stripe-amber { background: repeating-linear-gradient(135deg, var(--stripe-amber) 0 8px, var(--surface) 8px 16px); width: 96px; }
.stripe-blue { background: repeating-linear-gradient(135deg, var(--stripe-blue) 0 8px, var(--surface) 8px 16px); width: 120px; }
.stripe-mint { background: repeating-linear-gradient(135deg, var(--stripe-mint) 0 8px, var(--surface) 8px 16px); width: 96px; }
.kpi-mini-stat { font-size: 11px; color: var(--fg-tertiary); margin-top: 4px; text-align: right; }
.kpi-rooms-list { display: flex; flex-direction: column; gap: 6px; margin-top: auto; }
.kpi-rooms-list .row { display: flex; justify-content: space-between; font-size: 12px; }
.kpi-rooms-list .row .lbl { color: var(--fg-muted); display: flex; align-items: center; gap: 6px; }
.kpi-rooms-list .row .lbl::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-pill); background: var(--fg-tertiary); }
.kpi-rooms-list .row .val { color: var(--fg); font-weight: 600; }
.mid-row { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
@media (max-width: 1100px) { .mid-row { grid-template-columns: 1fr; } }
.card-pad { padding: 20px; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.card-title { display: flex; align-items: center; gap: 8px; font-family: var(--font-display); font-size: 15px; font-weight: 600; }
.card-title .ico-tile { width: 26px; height: 26px; border-radius: var(--r-sm); background: var(--surface-muted); color: var(--fg-muted); display: inline-flex; align-items: center; justify-content: center; }
.dropdown { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 6px 10px; font-size: 12px; color: var(--fg-muted); background: var(--surface); }
.legend { display: flex; gap: 16px; margin-bottom: 8px; flex-wrap: wrap; }
.legend .lg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-muted); }
.legend .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
.cal-head { display: flex; align-items: center; justify-content: space-between; }
.cal-month { font-family: var(--font-display); font-size: 16px; font-weight: 600; }
.cal-nav { color: var(--fg-muted); display: inline-flex; gap: 6px; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; text-align: center; font-size: 12px; margin-top: 10px; position: relative; }
.cal-grid .dow { color: var(--fg-tertiary); font-weight: 500; padding: 6px 0; font-size: 11px; }
.cal-grid .day { height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--r-pill); }
.cal-grid .day.muted { color: var(--fg-tertiary); }
.cal-grid .day.active { background: var(--accent); color: var(--surface); font-weight: 700; position: relative; }
.cal-grid .day.active::after { content: ""; position: absolute; bottom: -7px; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: var(--r-pill); background: var(--accent); }
.activity-popover { background: var(--surface-inverse); color: var(--fg-on-inverse); border-radius: var(--r-md); padding: 14px; box-shadow: var(--shadow-popover); margin-top: 14px; }
.activity-popover h6 { margin: 0 0 10px; font-size: 12px; font-weight: 600; }
.activity-popover .ev { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
.activity-popover .ev:last-of-type { border-bottom: none; }
.activity-popover .ev .av-pop { width: 22px; height: 22px; border-radius: var(--r-pill); flex-shrink: 0; }
.activity-popover .ev .av-pop.blue { background: linear-gradient(135deg, var(--av-blue-1), var(--av-blue-2)); }
.activity-popover .ev .av-pop.pink { background: linear-gradient(135deg, var(--av-pink-1), var(--av-pink-2)); }
.activity-popover .ev .av-pop.violet { background: linear-gradient(135deg, var(--av-violet-1), var(--av-violet-2)); }
.activity-popover .ev .av-pop.mint { background: linear-gradient(135deg, var(--av-mint-1), var(--av-mint-2)); }
.activity-popover .ev .av-pop.amber { background: linear-gradient(135deg, var(--av-amber-1), var(--av-amber-2)); }
.activity-popover .ev .name { flex: 1; font-size: 11px; }
.activity-popover .ev .time { font-size: 10px; color: rgba(248,250,252,0.6); }
.activity-popover .add { margin-top: 8px; font-size: 11px; color: rgba(248,250,252,0.7); }
.bottom-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; }
@media (max-width: 1100px) { .bottom-row { grid-template-columns: 1fr; } }
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 0; }
.donut { position: relative; width: 180px; height: 180px; }
.donut .center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.donut .center .lbl { font-size: 12px; color: var(--fg-tertiary); }
.donut .center .num { font-family: var(--font-display); font-size: 30px; font-weight: 700; line-height: 1; margin-top: 2px; }
.donut-legend { display: flex; gap: 18px; flex-wrap: wrap; justify-content: center; font-size: 12px; color: var(--fg-muted); }
.donut-legend .lg { display: inline-flex; align-items: center; gap: 6px; }
.donut-legend .lg .dot { width: 8px; height: 8px; border-radius: var(--r-pill); }
.donut-legend .lg .v { color: var(--fg); font-weight: 600; margin-right: 2px; }
.sched-stats { display: grid; grid-template-columns: 1fr 1fr 1fr; margin-bottom: 12px; }
.sched-stats .col { padding: 4px 12px; border-left: 1px solid var(--border); }
.sched-stats .col:first-child { border-left: none; padding-left: 0; }
.sched-stats .col .v { font-family: var(--font-display); font-size: 18px; font-weight: 700; line-height: 1.1; }
.sched-stats .col .v small { font-size: 11px; color: var(--fg-tertiary); font-weight: 400; margin-left: 2px; }
.sched-stats .col .lbl { font-size: 11px; color: var(--fg-tertiary); margin-top: 2px; }
.list-head { display: flex; align-items: center; gap: 4px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 11px; color: var(--fg-tertiary); font-weight: 500; }
.list-row { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.list-row:last-child { border-bottom: none; }
.person { flex: 1; display: flex; flex-direction: column; }
.person .n { font-size: 13px; font-weight: 600; }
.person .r { font-size: 11px; color: var(--fg-tertiary); }
.pill.list { padding: 3px 10px; }
.appt-time { display: flex; flex-direction: column; align-items: flex-end; }
.appt-time .d { font-size: 11px; color: var(--fg-tertiary); }
.appt-time .t { font-size: 13px; font-weight: 600; }
.ic { width: 18px; height: 18px; stroke-width: 1.6; }
.ic-sm { width: 14px; height: 14px; stroke-width: 1.6; }
</style>
</head>
<body data-od-id="clinic-console-template">
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<symbol id="icon-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></symbol>
<symbol id="icon-message" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 1 1 21 11.5z"/></symbol>
<symbol id="icon-schedule" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
<symbol id="icon-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></symbol>
<symbol id="icon-card" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="6" width="20" height="12" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></symbol>
<symbol id="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a8 8 0 0 1 16 0v1"/></symbol>
<symbol id="icon-pill" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10 2v4M14 2v4M5 10h14M6 6h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM12 13v6M9 16h6"/></symbol>
<symbol id="icon-bed" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 17h18M5 17v-5a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5"/></symbol>
<symbol id="icon-check-square" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M8 14l3 3 5-5"/></symbol>
<symbol id="icon-people" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="9" cy="8" r="3"/><path d="M3 21v-1a6 6 0 0 1 12 0v1"/><circle cx="17" cy="6" r="2.5"/><path d="M14 14a4 4 0 0 1 7 4"/></symbol>
<symbol id="icon-leaf" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 12c4-8 14-8 14 0 0 6-7 8-7 8s-7-2-7-8z"/></symbol>
<symbol id="icon-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/></symbol>
<symbol id="icon-stethoscope" viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 1 1 8 0v4"/></symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></symbol>
<symbol id="icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="icon-chev-left" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 18l-6-6 6-6"/></symbol>
<symbol id="icon-chev-right" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 18l6-6-6-6"/></symbol>
<symbol id="icon-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 9l6 6 6-6"/></symbol>
<symbol id="icon-collapse" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 6l-6 6 6 6M21 6l-6 6 6 6"/></symbol>
<symbol id="icon-logout" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
</defs>
</svg>
<aside class="sidebar" data-od-id="sidebar">
<div class="brand">
<div class="brand-mark">
<span class="brand-leaf"><svg class="ic-sm"><use href="#icon-leaf"/></svg></span>
{{data.brand_name}}
</div>
<span class="brand-toggle"><svg class="ic"><use href="#icon-collapse"/></svg></span>
</div>
<div class="nav-section">
<div class="nav-label">{{data.nav_main_label}}</div>
<a class="nav-item {{data.nav_main.0.active_class}}"><svg class="ic"><use href="#icon-dashboard"/></svg>{{data.nav_main.0.label}}<span class="count">{{data.nav_main.0.count}}</span></a>
<a class="nav-item {{data.nav_main.1.active_class}}"><svg class="ic"><use href="#icon-message"/></svg>{{data.nav_main.1.label}}<span class="count">{{data.nav_main.1.count}}</span></a>
<a class="nav-item {{data.nav_main.2.active_class}}"><svg class="ic"><use href="#icon-schedule"/></svg>{{data.nav_main.2.label}}<span class="count">{{data.nav_main.2.count}}</span></a>
<a class="nav-item {{data.nav_main.3.active_class}}"><svg class="ic"><use href="#icon-bell"/></svg>{{data.nav_main.3.label}}<span class="count">{{data.nav_main.3.count}}</span></a>
<a class="nav-item {{data.nav_main.4.active_class}}"><svg class="ic"><use href="#icon-card"/></svg>{{data.nav_main.4.label}}<span class="count">{{data.nav_main.4.count}}</span></a>
</div>
<div class="nav-section">
<div class="nav-label">{{data.nav_management_label}}</div>
<a class="nav-item {{data.nav_management.0.active_class}}"><svg class="ic"><use href="#icon-user"/></svg>{{data.nav_management.0.label}}<span class="count">{{data.nav_management.0.count}}</span></a>
<a class="nav-item {{data.nav_management.1.active_class}}"><svg class="ic"><use href="#icon-pill"/></svg>{{data.nav_management.1.label}}<span class="count">{{data.nav_management.1.count}}</span></a>
<a class="nav-item {{data.nav_management.2.active_class}}"><svg class="ic"><use href="#icon-bed"/></svg>{{data.nav_management.2.label}}<span class="count">{{data.nav_management.2.count}}</span></a>
<a class="nav-item {{data.nav_management.3.active_class}}"><svg class="ic"><use href="#icon-check-square"/></svg>{{data.nav_management.3.label}}<span class="count">{{data.nav_management.3.count}}</span></a>
<a class="nav-item {{data.nav_management.4.active_class}}"><svg class="ic"><use href="#icon-people"/></svg>{{data.nav_management.4.label}}<span class="count">{{data.nav_management.4.count}}</span></a>
</div>
<div class="pro-card">
<span class="pro-tag">{{data.pro_card.tag}}</span>
<h4>{{data.pro_card.title}}</h4>
<p>{{data.pro_card.body}}</p>
<div class="pro-actions">
<button class="primary">{{data.pro_card.primary_label}}</button>
<button>{{data.pro_card.secondary_label}}</button>
</div>
</div>
<div class="user-row">
<span class="av av-32 {{data.user.av_class}}">{{data.user.initial}}</span>
<div class="meta">
<span class="name">{{data.user.name}}</span>
<span class="role">{{data.user.role}}</span>
</div>
<span class="out"><svg class="ic"><use href="#icon-logout"/></svg></span>
</div>
</aside>
<main>
<header class="greeting-row" data-od-id="greeting">
<div class="greeting">{{data.greeting}}</div>
<div class="actions">
<div class="search">
<svg class="ic-sm"><use href="#icon-search"/></svg>
{{data.search_placeholder}}
<span class="kbd">{{data.search_shortcut}}</span>
</div>
<button class="btn"><svg class="ic-sm"><use href="#icon-download"/></svg>{{data.secondary_action_label}}</button>
<button class="btn primary"><svg class="ic-sm"><use href="#icon-plus"/></svg>{{data.primary_action_label}}</button>
</div>
</header>
<section class="kpi-strip" data-od-id="kpi-strip">
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-user"/></svg></span>
<span class="kpi-label">{{data.kpi_a.label}}</span>
<span class="kpi-menu">⋯</span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_a.value}}</div><span class="pill {{data.kpi_a.trend_class}}">{{data.kpi_a.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_a.caption}}</div>
<div class="kpi-strip-pat {{data.kpi_a.strip_class}}"></div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
<span class="kpi-label">{{data.kpi_b.label}}</span>
<span class="kpi-menu">⋯</span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_b.value}}</div><span class="pill {{data.kpi_b.trend_class}}">{{data.kpi_b.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_b.caption}}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div class="kpi-strip-pat {{data.kpi_b.strip_class}}"></div>
<div class="kpi-mini-stat">{{data.kpi_b.mini_stat}}</div>
</div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-bed"/></svg></span>
<span class="kpi-label">{{data.kpi_c.label}}</span>
<span class="kpi-menu">⋯</span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_c.value}}</div><span class="pill {{data.kpi_c.trend_class}}">{{data.kpi_c.trend_label}}</span></div>
<div class="kpi-rooms-list">
<div class="row"><span class="lbl">{{data.kpi_c.rows.0.label}}</span><span class="val">{{data.kpi_c.rows.0.value}}</span></div>
<div class="row"><span class="lbl">{{data.kpi_c.rows.1.label}}</span><span class="val">{{data.kpi_c.rows.1.value}}</span></div>
</div>
</div>
<div class="kpi">
<div class="kpi-head">
<span class="kpi-glyph"><svg class="ic-sm"><use href="#icon-people"/></svg></span>
<span class="kpi-label">{{data.kpi_d.label}}</span>
<span class="kpi-menu">⋯</span>
</div>
<div class="kpi-num-row"><div class="kpi-num">{{data.kpi_d.value}}</div><span class="pill {{data.kpi_d.trend_class}}">{{data.kpi_d.trend_label}}</span></div>
<div class="kpi-foot">
<div class="kpi-cap">{{data.kpi_d.caption}}</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;">
<div class="kpi-strip-pat {{data.kpi_d.strip_class}}"></div>
<div class="kpi-mini-stat">{{data.kpi_d.mini_stat}}</div>
</div>
</div>
</div>
</section>
<section class="mid-row" data-od-id="patient-overview-and-calendar">
<div class="card card-pad">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-clock"/></svg></span>
{{data.chart.title}}
</div>
<div class="dropdown">{{data.chart.dropdown_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
</div>
<div class="legend">
<span class="lg"><span class="dot" style="background: var(--accent);"></span>{{data.chart.legend_a}}</span>
<span class="lg"><span class="dot" style="background: var(--legend-blue);"></span>{{data.chart.legend_b}}</span>
<span class="lg"><span class="dot" style="background: var(--legend-amber);"></span>{{data.chart.legend_c}}</span>
</div>
<svg viewBox="0 0 600 240" preserveAspectRatio="none" style="width:100%; height:240px; display:block;">
<defs>
<pattern id="stripeBlue" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
<rect width="12" height="12" style="fill: var(--surface);" />
<rect width="6" height="12" style="fill: var(--stripe-blue);" />
</pattern>
<pattern id="stripeMint" width="12" height="12" patternUnits="userSpaceOnUse" patternTransform="rotate(-45)">
<rect width="12" height="12" style="fill: var(--surface);" />
<rect width="6" height="12" style="fill: var(--stripe-mint);" opacity="0.7" />
</pattern>
</defs>
<g style="stroke: var(--border);" stroke-width="1">
<line x1="0" y1="48" x2="600" y2="48" />
<line x1="0" y1="96" x2="600" y2="96" />
<line x1="0" y1="144" x2="600" y2="144" />
<line x1="0" y1="192" x2="600" y2="192" />
</g>
<g>
<rect x="{{data.chart.bars.0.x}}" y="{{data.chart.bars.0.y}}" width="22" height="{{data.chart.bars.0.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.1.x}}" y="{{data.chart.bars.1.y}}" width="22" height="{{data.chart.bars.1.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.2.x}}" y="{{data.chart.bars.2.y}}" width="22" height="{{data.chart.bars.2.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.3.x}}" y="{{data.chart.bars.3.y}}" width="22" height="{{data.chart.bars.3.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.4.x}}" y="{{data.chart.bars.4.y}}" width="22" height="{{data.chart.bars.4.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.5.x}}" y="{{data.chart.bars.5.y}}" width="22" height="{{data.chart.bars.5.h}}" rx="6" fill="url(#stripeBlue)" style="stroke: var(--accent);" stroke-width="2" />
<rect x="{{data.chart.bars.6.x}}" y="{{data.chart.bars.6.y}}" width="22" height="{{data.chart.bars.6.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.7.x}}" y="{{data.chart.bars.7.y}}" width="22" height="{{data.chart.bars.7.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.8.x}}" y="{{data.chart.bars.8.y}}" width="22" height="{{data.chart.bars.8.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.9.x}}" y="{{data.chart.bars.9.y}}" width="22" height="{{data.chart.bars.9.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.10.x}}" y="{{data.chart.bars.10.y}}" width="22" height="{{data.chart.bars.10.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.11.x}}" y="{{data.chart.bars.11.y}}" width="22" height="{{data.chart.bars.11.h}}" rx="6" fill="url(#stripeBlue)" />
<rect x="{{data.chart.bars.12.x}}" y="{{data.chart.bars.12.y}}" width="22" height="{{data.chart.bars.12.h}}" rx="6" fill="url(#stripeMint)" />
<rect x="{{data.chart.bars.13.x}}" y="{{data.chart.bars.13.y}}" width="22" height="{{data.chart.bars.13.h}}" rx="6" fill="url(#stripeBlue)" />
</g>
<g style="fill: var(--fg-tertiary);" font-size="11" text-anchor="middle">
<text x="46" y="232">{{data.chart.x_labels.0}}</text>
<text x="132" y="232">{{data.chart.x_labels.1}}</text>
<text x="218" y="232">{{data.chart.x_labels.2}}</text>
<text x="304" y="232">{{data.chart.x_labels.3}}</text>
<text x="390" y="232">{{data.chart.x_labels.4}}</text>
<text x="476" y="232">{{data.chart.x_labels.5}}</text>
<text x="562" y="232">{{data.chart.x_labels.6}}</text>
</g>
</svg>
</div>
<div class="card card-pad" data-od-id="calendar">
<div class="cal-head">
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-left"/></svg></span>
<div class="cal-month">{{data.calendar.month_label}}</div>
<span class="cal-nav"><svg class="ic-sm"><use href="#icon-chev-right"/></svg></span>
</div>
<div class="cal-grid">
<div class="dow">{{data.calendar.dow.0}}</div><div class="dow">{{data.calendar.dow.1}}</div><div class="dow">{{data.calendar.dow.2}}</div><div class="dow">{{data.calendar.dow.3}}</div><div class="dow">{{data.calendar.dow.4}}</div><div class="dow">{{data.calendar.dow.5}}</div><div class="dow">{{data.calendar.dow.6}}</div>
<div class="day {{data.calendar.days.0.modifier}}">{{data.calendar.days.0.label}}</div><div class="day {{data.calendar.days.1.modifier}}">{{data.calendar.days.1.label}}</div><div class="day {{data.calendar.days.2.modifier}}">{{data.calendar.days.2.label}}</div><div class="day {{data.calendar.days.3.modifier}}">{{data.calendar.days.3.label}}</div><div class="day {{data.calendar.days.4.modifier}}">{{data.calendar.days.4.label}}</div><div class="day {{data.calendar.days.5.modifier}}">{{data.calendar.days.5.label}}</div><div class="day {{data.calendar.days.6.modifier}}">{{data.calendar.days.6.label}}</div>
<div class="day {{data.calendar.days.7.modifier}}">{{data.calendar.days.7.label}}</div><div class="day {{data.calendar.days.8.modifier}}">{{data.calendar.days.8.label}}</div><div class="day {{data.calendar.days.9.modifier}}">{{data.calendar.days.9.label}}</div><div class="day {{data.calendar.days.10.modifier}}">{{data.calendar.days.10.label}}</div><div class="day {{data.calendar.days.11.modifier}}">{{data.calendar.days.11.label}}</div><div class="day {{data.calendar.days.12.modifier}}">{{data.calendar.days.12.label}}</div><div class="day {{data.calendar.days.13.modifier}}">{{data.calendar.days.13.label}}</div>
<div class="day {{data.calendar.days.14.modifier}}">{{data.calendar.days.14.label}}</div><div class="day {{data.calendar.days.15.modifier}}">{{data.calendar.days.15.label}}</div><div class="day {{data.calendar.days.16.modifier}}">{{data.calendar.days.16.label}}</div><div class="day {{data.calendar.days.17.modifier}}">{{data.calendar.days.17.label}}</div><div class="day {{data.calendar.days.18.modifier}}">{{data.calendar.days.18.label}}</div><div class="day {{data.calendar.days.19.modifier}}">{{data.calendar.days.19.label}}</div><div class="day {{data.calendar.days.20.modifier}}">{{data.calendar.days.20.label}}</div>
<div class="day {{data.calendar.days.21.modifier}}">{{data.calendar.days.21.label}}</div><div class="day {{data.calendar.days.22.modifier}}">{{data.calendar.days.22.label}}</div><div class="day {{data.calendar.days.23.modifier}}">{{data.calendar.days.23.label}}</div><div class="day {{data.calendar.days.24.modifier}}">{{data.calendar.days.24.label}}</div><div class="day {{data.calendar.days.25.modifier}}">{{data.calendar.days.25.label}}</div><div class="day {{data.calendar.days.26.modifier}}">{{data.calendar.days.26.label}}</div><div class="day {{data.calendar.days.27.modifier}}">{{data.calendar.days.27.label}}</div>
<div class="day {{data.calendar.days.28.modifier}}">{{data.calendar.days.28.label}}</div><div class="day {{data.calendar.days.29.modifier}}">{{data.calendar.days.29.label}}</div><div class="day {{data.calendar.days.30.modifier}}">{{data.calendar.days.30.label}}</div><div class="day {{data.calendar.days.31.modifier}}">{{data.calendar.days.31.label}}</div><div class="day {{data.calendar.days.32.modifier}}">{{data.calendar.days.32.label}}</div><div class="day {{data.calendar.days.33.modifier}}">{{data.calendar.days.33.label}}</div><div class="day {{data.calendar.days.34.modifier}}">{{data.calendar.days.34.label}}</div>
</div>
<div class="activity-popover" data-od-id="activity-popover">
<h6>{{data.activity.title}}</h6>
<div class="ev"><span class="av-pop {{data.activity.events.0.av_class}}"></span><span class="name">{{data.activity.events.0.name}}</span><span class="time">{{data.activity.events.0.time}}</span></div>
<div class="ev"><span class="av-pop {{data.activity.events.1.av_class}}"></span><span class="name">{{data.activity.events.1.name}}</span><span class="time">{{data.activity.events.1.time}}</span></div>
<div class="ev"><span class="av-pop {{data.activity.events.2.av_class}}"></span><span class="name">{{data.activity.events.2.name}}</span><span class="time">{{data.activity.events.2.time}}</span></div>
<div class="add">{{data.activity.add_label}}</div>
</div>
</div>
</section>
<section class="bottom-row">
<div class="card card-pad" data-od-id="top-clinics">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-stethoscope"/></svg></span>
{{data.donut.title}}
</div>
<span class="kpi-menu">⋯</span>
</div>
<div class="donut-wrap">
<div class="donut">
<svg viewBox="0 0 100 100" style="width:100%; height:100%; transform: rotate(-90deg);">
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-blue);" />
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-pink);" stroke-dasharray="{{data.donut.segment_b.dasharray}}" stroke-dashoffset="{{data.donut.segment_b.dashoffset}}" />
<circle cx="50" cy="50" r="38" fill="none" stroke-width="14" style="stroke: var(--donut-mint);" stroke-dasharray="{{data.donut.segment_c.dasharray}}" stroke-dashoffset="{{data.donut.segment_c.dashoffset}}" />
</svg>
<div class="center">
<span class="lbl">{{data.donut.center_label}}</span>
<span class="num">{{data.donut.center_num}}</span>
</div>
</div>
<div class="donut-legend">
<div class="lg"><span class="dot" style="background: var(--legend-blue);"></span><span class="v">{{data.donut.legend.0.value}}</span> {{data.donut.legend.0.label}}</div>
<div class="lg"><span class="dot" style="background: var(--legend-pink);"></span><span class="v">{{data.donut.legend.1.value}}</span> {{data.donut.legend.1.label}}</div>
<div class="lg"><span class="dot" style="background: var(--legend-mint);"></span><span class="v">{{data.donut.legend.2.value}}</span> {{data.donut.legend.2.label}}</div>
</div>
</div>
</div>
<div class="card card-pad" data-od-id="doctor-schedule">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-schedule"/></svg></span>
{{data.schedule.title}}
</div>
<span class="kpi-menu">⋯</span>
</div>
<div class="sched-stats">
<div class="col"><div class="v">{{data.schedule.stats.0.value}} <small>{{data.schedule.stats.0.small}}</small></div><div class="lbl">{{data.schedule.stats.0.label}}</div></div>
<div class="col"><div class="v">{{data.schedule.stats.1.value}} <small>{{data.schedule.stats.1.small}}</small></div><div class="lbl">{{data.schedule.stats.1.label}}</div></div>
<div class="col"><div class="v">{{data.schedule.stats.2.value}} <small>{{data.schedule.stats.2.small}}</small></div><div class="lbl">{{data.schedule.stats.2.label}}</div></div>
</div>
<div class="list-head">{{data.schedule.list_header_label}}<svg class="ic-sm"><use href="#icon-chev-down"/></svg></div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.0.av_class}}">{{data.schedule.doctors.0.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.0.name}}</span><span class="r">{{data.schedule.doctors.0.role}}</span></div>
<span class="pill list {{data.schedule.doctors.0.status_class}}">{{data.schedule.doctors.0.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.1.av_class}}">{{data.schedule.doctors.1.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.1.name}}</span><span class="r">{{data.schedule.doctors.1.role}}</span></div>
<span class="pill list {{data.schedule.doctors.1.status_class}}">{{data.schedule.doctors.1.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.2.av_class}}">{{data.schedule.doctors.2.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.2.name}}</span><span class="r">{{data.schedule.doctors.2.role}}</span></div>
<span class="pill list {{data.schedule.doctors.2.status_class}}">{{data.schedule.doctors.2.status_label}}</span>
</div>
<div class="list-row">
<span class="av av-32 {{data.schedule.doctors.3.av_class}}">{{data.schedule.doctors.3.initial}}</span>
<div class="person"><span class="n">{{data.schedule.doctors.3.name}}</span><span class="r">{{data.schedule.doctors.3.role}}</span></div>
<span class="pill list {{data.schedule.doctors.3.status_class}}">{{data.schedule.doctors.3.status_label}}</span>
</div>
</div>
<div class="card card-pad" data-od-id="appointments">
<div class="card-head">
<div class="card-title">
<span class="ico-tile"><svg class="ic-sm"><use href="#icon-check-square"/></svg></span>
{{data.appointments.title}}
</div>
<span class="kpi-menu">⋯</span>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.0.av_class}}">{{data.appointments.list.0.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.0.name}}</span><span class="r">{{data.appointments.list.0.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.0.date}}</span><span class="t">{{data.appointments.list.0.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.1.av_class}}">{{data.appointments.list.1.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.1.name}}</span><span class="r">{{data.appointments.list.1.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.1.date}}</span><span class="t">{{data.appointments.list.1.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.2.av_class}}">{{data.appointments.list.2.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.2.name}}</span><span class="r">{{data.appointments.list.2.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.2.date}}</span><span class="t">{{data.appointments.list.2.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.3.av_class}}">{{data.appointments.list.3.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.3.name}}</span><span class="r">{{data.appointments.list.3.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.3.date}}</span><span class="t">{{data.appointments.list.3.time}}</span></div>
</div>
<div class="list-row">
<span class="av av-40 {{data.appointments.list.4.av_class}}">{{data.appointments.list.4.initial}}</span>
<div class="person"><span class="n">{{data.appointments.list.4.name}}</span><span class="r">{{data.appointments.list.4.role}}</span></div>
<div class="appt-time"><span class="d">{{data.appointments.list.4.date}}</span><span class="t">{{data.appointments.list.4.time}}</span></div>
</div>
</div>
</section>
</main>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mira's First Month · A quiet panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600&family=Caveat:wght@500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Soft, warm, parental */
--bg: #fdf9f5;
--paper: #ffffff;
--paper-tint: #faf5ef;
--paper-deep: #f4ece2;
--line: #ece2d5;
--line-strong: #d8c8b3;
--hairline: rgba(60,40,20,0.06);
--ink: #2a2520;
--ink-soft: #4d433b;
--muted: #8a7e72;
--muted-2: #b3a89a;
/* Soft pastel accents */
--peach: #f5b885;
--peach-soft: #fce4cf;
--sage: #9ec79e;
--sage-soft: #e0eddc;
--lavender: #c5b8e0;
--lavender-soft: #e8e1f5;
--sky: #b3d4e8;
--sky-soft: #def0fa;
--rose: #e8b3c2;
--rose-soft: #fadfe6;
--gold: #e8d094;
--gold-soft: #faf1d6;
/* Type */
--serif: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--display: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--hand: 'Caveat', 'Instrument Serif', cursive;
--r: 16px; /* softer corners */
--r-pill: 999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background:
radial-gradient(800px 400px at 100% -10%, rgba(245,184,133,0.15), transparent 60%),
radial-gradient(700px 400px at 0% 60%, rgba(197,184,224,0.12), transparent 60%),
radial-gradient(900px 500px at 50% 110%, rgba(158,199,158,0.10), transparent 60%),
var(--bg);
color: var(--ink);
font-family: var(--body);
font-size: 14px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
/* ─────── Layout ─────── */
main {
max-width: 880px; margin: 0 auto;
padding: 60px 32px 80px;
}
header.app {
display: flex; align-items: center; justify-content: space-between;
padding: 22px 32px;
border-bottom: 1px solid var(--hairline);
}
.word-mark {
font-family: var(--serif); font-style: italic;
font-size: 19px; letter-spacing: -0.005em;
color: var(--ink);
}
.word-mark .small {
font-family: var(--body); font-style: normal;
font-size: 11px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.16em;
margin-right: 10px;
}
.icon-btn {
padding: 7px 14px; font-size: 12.5px;
border-radius: var(--r-pill);
background: var(--paper); border: 1px solid var(--line);
color: var(--ink-soft); transition: all 0.15s;
display: inline-flex; gap: 8px; align-items: center;
}
.icon-btn:hover { border-color: var(--line-strong); color: var(--ink); }
.icon-btn.warm {
background: linear-gradient(180deg, #fef4e6, var(--paper));
border-color: rgba(245,184,133,0.45);
color: #8a5a2a;
}
.icon-btn .ic { width: 13px; height: 13px; }
.icon-btn.spin .ic { animation: spin 0.8s linear; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ─────── Hero ─────── */
.hero {
text-align: center;
margin-bottom: 56px;
}
.hero .pre {
font-size: 11px; color: var(--muted); text-transform: uppercase;
letter-spacing: 0.2em; margin-bottom: 18px;
}
.hero .name {
font-family: var(--serif); font-weight: 400;
font-size: 80px; line-height: 1.05;
margin: 0; letter-spacing: -0.02em;
color: var(--ink);
}
.hero .name em {
font-style: italic;
background: linear-gradient(180deg, #f5b885 0%, #c5b8e0 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero .age {
font-family: var(--hand);
font-size: 32px; line-height: 1;
color: var(--peach);
margin-top: 14px;
transform: rotate(-2deg);
display: inline-block;
}
.hero .meta {
margin-top: 22px;
display: inline-flex; gap: 18px;
font-size: 13px; color: var(--muted);
}
.hero .meta .v { color: var(--ink); }
.hero .meta .sep { color: var(--muted-2); }
/* ─────── Section ─────── */
section.block {
margin-bottom: 44px;
}
.block-head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 18px;
}
.block-head h2 {
font-family: var(--serif); font-weight: 400; font-size: 30px;
margin: 0; line-height: 1; letter-spacing: -0.005em;
color: var(--ink);
}
.block-head h2 em {
font-style: italic; color: var(--peach);
margin-right: 8px; font-size: 21px; vertical-align: 4px;
}
.block-head .meta {
font-size: 11.5px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.14em;
}
.block-head .meta .src { color: var(--ink-soft); margin-right: 4px; }
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 28px;
box-shadow: 0 2px 0 rgba(60,40,20,0.02);
}
/* ─────── Sleep circle ─────── */
.sleep-card {
display: grid; grid-template-columns: 280px 1fr; gap: 28px; align-items: center;
}
.sleep-circle {
width: 280px; height: 280px;
margin: 0 auto;
}
.sleep-circle svg { width: 100%; height: 100%; display: block; }
.sleep-stat .lbl {
font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em;
}
.sleep-stat .big {
font-family: var(--serif); font-weight: 400; font-size: 56px;
line-height: 1; margin-top: 8px; letter-spacing: -0.02em;
}
.sleep-stat .delta {
margin-top: 10px; font-size: 13px; color: var(--sage);
}
.sleep-stat .delta strong { color: var(--ink); font-weight: 600; }
.sleep-stat .quote {
font-family: var(--serif); font-style: italic;
font-size: 17px; color: var(--ink-soft);
margin-top: 22px; padding-left: 14px;
border-left: 2px solid var(--lavender);
line-height: 1.5;
max-width: 36ch;
}
/* ─────── Feeding timeline ─────── */
.feed-timeline {
position: relative; padding: 28px 0 32px;
}
.feed-line {
height: 2px;
background: linear-gradient(90deg, var(--lavender), var(--peach), var(--sage));
border-radius: 2px;
position: relative;
margin: 0 14px;
}
.feed-marker {
position: absolute; top: 50%; transform: translate(-50%, -50%);
width: 14px; height: 14px; border-radius: 50%;
cursor: pointer;
transition: transform 0.15s;
}
.feed-marker:hover { transform: translate(-50%, -50%) scale(1.4); }
.feed-marker.bottle { background: var(--peach); box-shadow: 0 0 0 4px var(--peach-soft); }
.feed-marker.breast { background: var(--rose); box-shadow: 0 0 0 4px var(--rose-soft); }
.feed-marker.now {
background: var(--ink); box-shadow: 0 0 0 4px var(--paper), 0 0 0 6px var(--ink);
animation: nowBreath 2.4s ease-in-out infinite;
}
@keyframes nowBreath {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.15); }
}
.feed-tip {
position: absolute; top: -36px; transform: translate(-50%, 0);
background: var(--ink); color: var(--paper);
font-size: 11px; padding: 4px 8px; border-radius: 5px;
white-space: nowrap;
opacity: 0; pointer-events: none; transition: opacity 0.15s;
font-family: var(--body);
}
.feed-tip::after {
content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%);
border: 4px solid transparent; border-top-color: var(--ink);
}
.feed-marker:hover .feed-tip { opacity: 1; }
.feed-axis {
margin-top: 36px; padding: 0 14px;
display: grid; grid-template-columns: repeat(7, 1fr);
font-family: var(--body); font-size: 11px; color: var(--muted);
text-align: center;
}
.feed-legend {
margin-top: 24px;
display: flex; gap: 22px; flex-wrap: wrap;
font-size: 12px; color: var(--ink-soft);
}
.feed-legend .swatch { display: inline-flex; gap: 8px; align-items: center; }
.feed-legend .swatch::before { content: ""; width: 10px; height: 10px; border-radius: 50%; }
.feed-legend .swatch.bottle::before { background: var(--peach); box-shadow: 0 0 0 3px var(--peach-soft); }
.feed-legend .swatch.breast::before { background: var(--rose); box-shadow: 0 0 0 3px var(--rose-soft); }
.feed-legend .swatch.now::before { background: var(--ink); }
.feed-legend .v { font-family: var(--serif); font-style: italic; color: var(--ink); }
/* ─────── Weight curve ─────── */
.growth-card { padding: 28px 32px; }
.growth-card svg { width: 100%; height: 220px; display: block; margin-top: 8px; }
.growth-stats {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--hairline);
}
.growth-stats .stat .lbl { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em; }
.growth-stats .stat .v { font-family: var(--serif); font-size: 24px; line-height: 1; margin-top: 6px; letter-spacing: -0.01em; }
.growth-stats .stat .pctile { color: var(--sage); font-size: 11.5px; margin-top: 4px; }
/* ─────── Today reminders ─────── */
.reminders {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
}
.rem-card {
padding: 18px 20px; border-radius: 12px;
border: 1px solid var(--line);
background: var(--paper);
display: flex; flex-direction: column; gap: 8px;
cursor: pointer;
transition: all 0.15s;
}
.rem-card:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(60,40,20,0.06); }
.rem-card .l-row { display: flex; gap: 10px; align-items: center; color: var(--muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.14em; }
.rem-card .glyph {
width: 28px; height: 28px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 14px;
}
.rem-card.feed .glyph { background: var(--peach-soft); color: #8a5a2a; }
.rem-card.nap .glyph { background: var(--lavender-soft); color: #5e4d8e; }
.rem-card.appt .glyph { background: var(--sky-soft); color: #2a5e7a; }
.rem-card.bath .glyph { background: var(--sage-soft); color: #4a6e4a; }
.rem-card .when {
font-family: var(--serif); font-size: 28px; line-height: 1; letter-spacing: -0.01em;
}
.rem-card .desc { font-size: 12.5px; color: var(--ink-soft); line-height: 1.45; }
/* ─────── AI insights ─────── */
.insight-card {
background: linear-gradient(180deg, var(--lavender-soft) 0%, var(--paper) 100%);
border-color: rgba(197,184,224,0.4);
padding: 28px 32px;
}
.insight-card .badge {
display: inline-flex; gap: 8px; align-items: center;
font-family: var(--body); font-size: 10.5px;
text-transform: uppercase; letter-spacing: 0.16em;
color: #6e5e9e;
}
.insight-card .badge .glyph {
width: 22px; height: 22px; border-radius: 50%;
background: linear-gradient(135deg, var(--lavender), var(--peach));
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-size: 12px; font-weight: 600;
box-shadow: 0 0 12px rgba(197,184,224,0.4);
}
.insight-card h3 {
font-family: var(--serif); font-weight: 400; font-size: 28px;
line-height: 1.2; margin: 12px 0 6px; letter-spacing: -0.01em;
}
.insight-card h3 em { font-style: italic; color: #6e5e9e; }
.insight-card p {
color: var(--ink-soft); font-size: 14px; margin: 0;
line-height: 1.55; max-width: 56ch;
}
.insight-list {
margin-top: 16px; display: flex; flex-direction: column; gap: 10px;
}
.insight-list .row {
display: grid; grid-template-columns: 22px 1fr; gap: 10px;
align-items: flex-start;
}
.insight-list .check {
color: var(--sage); font-family: var(--serif); font-size: 18px; line-height: 1;
}
.insight-list .text { font-size: 13px; color: var(--ink-soft); line-height: 1.5; }
.insight-list .text strong { color: var(--ink); }
.insight-list .text .hand {
font-family: var(--hand); font-size: 16px; color: var(--peach);
transform: rotate(-1deg); display: inline-block; padding: 0 4px;
}
/* ─────── Photo strip (placeholder) ─────── */
.photo-strip {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
}
.photo {
aspect-ratio: 4 / 5; border-radius: 10px;
background: linear-gradient(135deg, var(--peach-soft), var(--lavender-soft));
border: 1px solid var(--line);
position: relative;
overflow: hidden;
cursor: pointer;
transition: transform 0.15s;
}
.photo:hover { transform: translateY(-2px) rotate(-1deg); }
.photo:nth-child(2) { background: linear-gradient(135deg, var(--rose-soft), var(--sky-soft)); transform: rotate(0.5deg); }
.photo:nth-child(3) { background: linear-gradient(135deg, var(--sage-soft), var(--gold-soft)); transform: rotate(-0.5deg); }
.photo:nth-child(4) { background: linear-gradient(135deg, var(--gold-soft), var(--peach-soft)); transform: rotate(0.8deg); }
.photo .cap {
position: absolute; bottom: 8px; left: 10px; right: 10px;
font-family: var(--hand); font-size: 16px;
color: var(--ink-soft); transform: rotate(-1deg);
}
.photo .day {
position: absolute; top: 8px; right: 10px;
font-family: var(--body); font-size: 10.5px; color: var(--muted);
background: rgba(255,255,255,0.7); padding: 2px 6px; border-radius: 3px;
backdrop-filter: blur(2px);
}
/* Closing line */
.closing {
text-align: center; margin-top: 40px;
font-family: var(--serif); font-style: italic;
font-size: 18px; color: var(--muted);
line-height: 1.5;
}
.closing .author {
font-family: var(--body); font-style: normal;
font-size: 11px; color: var(--muted-2);
text-transform: uppercase; letter-spacing: 0.16em;
margin-top: 8px;
}
/* Toast */
.toast-box { position: fixed; bottom: 24px; right: 24px; z-index: 200; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
.toast {
pointer-events: auto;
min-width: 280px; max-width: 380px;
padding: 14px 16px;
background: var(--paper);
border: 1px solid var(--line);
border-left: 3px solid var(--peach);
border-radius: 12px;
box-shadow: 0 16px 40px rgba(60,40,20,0.10);
font-size: 13px;
color: var(--ink);
animation: toastIn 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.lavender { border-left-color: var(--lavender); }
.toast.sage { border-left-color: var(--sage); }
.toast .t-title { font-family: var(--serif); font-size: 16px; line-height: 1.2; margin-bottom: 4px; }
.toast .t-body { color: var(--ink-soft); line-height: 1.45; }
.toast .t-body strong { color: var(--ink); }
.toast.fade { opacity: 0; transform: translateX(20px); transition: all 0.25s; }
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
@media (max-width: 720px) {
main { padding: 40px 18px 60px; }
.hero .name { font-size: 56px; }
.sleep-card { grid-template-columns: 1fr; }
.sleep-circle { width: 220px; height: 220px; }
.reminders { grid-template-columns: 1fr; }
.photo-strip { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<header class="app">
<div class="word-mark">
<span class="small">a quiet panel from</span>
<em>quiver</em>
</div>
<button class="icon-btn warm" id="logBtn">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
Log a feed
</button>
</header>
<main>
<!-- ─────── Hero ─────── -->
<div class="hero">
<div class="pre">Mira's first month · 2026</div>
<h1 class="name">Mira <em>Avery</em></h1>
<div class="age">28 days & counting</div>
<div class="meta">
<span><span class="v">Born</span> April 9 · 2026</span>
<span class="sep">·</span>
<span><span class="v">3.42 kg</span> · 50.8 cm</span>
<span class="sep">·</span>
<span><span class="v">Mom & dad</span> have her back</span>
</div>
</div>
<!-- ─────── 1. Sleep ─────── -->
<section class="block">
<div class="block-head">
<h2><em>i.</em>How she slept last night</h2>
<span class="meta"><span class="src">Huckleberry</span>· synced 6:42 AM</span>
</div>
<div class="card sleep-card">
<div class="sleep-circle">
<svg viewBox="0 0 200 200">
<defs>
<radialGradient id="bgG" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#c5b8e0" stop-opacity="0.06"/>
</radialGradient>
</defs>
<circle cx="100" cy="100" r="92" fill="url(#bgG)" stroke="#ece2d5" stroke-width="0.8"/>
<!-- 24h tick marks -->
<g stroke="#d8c8b3" stroke-width="0.6">
<line x1="100" y1="14" x2="100" y2="20"/>
<line x1="186" y1="100" x2="180" y2="100"/>
<line x1="100" y1="186" x2="100" y2="180"/>
<line x1="14" y1="100" x2="20" y2="100"/>
</g>
<!-- Hour labels -->
<g font-family="Inter,sans-serif" font-size="9" fill="#8a7e72">
<text x="100" y="11" text-anchor="middle">12 am</text>
<text x="194" y="103" text-anchor="end">6</text>
<text x="100" y="197" text-anchor="middle">12 pm</text>
<text x="6" y="103">6</text>
</g>
<!-- Sleep arcs (purple = night, peach = naps) -->
<!-- Night sleep 7:30 PM → 6:30 AM (with 4 brief wakes) -->
<!-- Stroke arcs at radius 76 -->
<path d="M 154.5 145.5 A 76 76 0 0 1 100 24" fill="none" stroke="#c5b8e0" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<path d="M 100 24 A 76 76 0 0 1 134.7 32.5" fill="none" stroke="#c5b8e0" stroke-width="14" stroke-linecap="round" opacity="0.5"/>
<!-- 1st morning nap 9:00–10:30 -->
<path d="M 162.4 76.6 A 76 76 0 0 1 173.1 105.4" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<!-- afternoon nap 1:30–3:00 -->
<path d="M 142.1 168.6 A 76 76 0 0 1 100 176" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<!-- evening nap 6:00–6:45 -->
<path d="M 24 100 A 76 76 0 0 1 32.5 65.3" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.65"/>
<!-- Center text -->
<text x="100" y="92" text-anchor="middle" font-family="Instrument Serif,serif" font-size="34" fill="#2a2520" letter-spacing="-0.5">14h 22m</text>
<text x="100" y="115" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#8a7e72" letter-spacing="0.6">total · 24h window</text>
<!-- now hand -->
<line x1="100" y1="100" x2="100" y2="62" stroke="#2a2520" stroke-width="1.5" stroke-linecap="round" transform="rotate(105 100 100)"/>
<circle cx="100" cy="100" r="3" fill="#2a2520"/>
</svg>
</div>
<div class="sleep-stat">
<div class="lbl">Last night · 7:30 PM → 6:30 AM</div>
<div class="big">9h 14m</div>
<div class="delta">3 wakes (down from 4) · longest stretch <strong>3h 12m</strong></div>
<div class="quote">"She slept 22 minutes longer per night this week. Not a streak — a rhythm."</div>
</div>
</div>
</section>
<!-- ─────── 2. Feeding ─────── -->
<section class="block">
<div class="block-head">
<h2><em>ii.</em>How she ate today</h2>
<span class="meta"><span class="src">Baby Tracker + scale</span>· last 24h</span>
</div>
<div class="card">
<div class="feed-timeline">
<div class="feed-line">
<!-- timeline runs midnight → midnight → markers as % -->
<div class="feed-marker bottle" style="left:8.3%"><span class="feed-tip">2:00 AM · 90ml bottle</span></div>
<div class="feed-marker breast" style="left:21%"><span class="feed-tip">5:00 AM · breast 18 min</span></div>
<div class="feed-marker bottle" style="left:33%"><span class="feed-tip">8:00 AM · 110ml bottle</span></div>
<div class="feed-marker breast" style="left:46%"><span class="feed-tip">11:00 AM · breast 22 min</span></div>
<div class="feed-marker bottle" style="left:58%"><span class="feed-tip">2:00 PM · 100ml bottle</span></div>
<div class="feed-marker now" style="left:60.5%"><span class="feed-tip">Now · 14:32</span></div>
<div class="feed-marker breast" style="left:71%"><span class="feed-tip">5:00 PM · breast 16 min (est.)</span></div>
<div class="feed-marker bottle" style="left:83%"><span class="feed-tip">8:00 PM · 100ml bottle (est.)</span></div>
<div class="feed-marker breast" style="left:96%"><span class="feed-tip">11:30 PM · breast (est.)</span></div>
</div>
<div class="feed-axis">
<span>12am</span><span>4am</span><span>8am</span><span>noon</span><span>4pm</span><span>8pm</span><span>12am</span>
</div>
<div class="feed-legend">
<span class="swatch bottle">Bottle <span class="v" style="margin-left:4px;">4 today · 400 ml</span></span>
<span class="swatch breast">Breast <span class="v" style="margin-left:4px;">5 today · ~110 min</span></span>
<span class="swatch now">Now</span>
</div>
</div>
</div>
</section>
<!-- ─────── 3. Weight curve ─────── -->
<section class="block">
<div class="block-head">
<h2><em>iii.</em>How she's growing</h2>
<span class="meta"><span class="src">Withings smart scale</span>· weekly</span>
</div>
<div class="card growth-card">
<svg viewBox="0 0 600 220" preserveAspectRatio="none">
<defs>
<linearGradient id="growthArea" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#9ec79e" stop-opacity="0.28"/>
<stop offset="100%" stop-color="#9ec79e" stop-opacity="0"/>
</linearGradient>
<linearGradient id="bandG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#c5b8e0" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#c5b8e0" stop-opacity="0.04"/>
</linearGradient>
</defs>
<!-- WHO percentile band (10–90) -->
<polygon fill="url(#bandG)" points="40,170 120,154 200,138 280,122 360,108 440,96 520,84 580,76 580,140 520,150 440,160 360,168 280,176 200,182 120,188 40,194"/>
<!-- 50th percentile dashed line -->
<polyline fill="none" stroke="#c5b8e0" stroke-width="1" stroke-dasharray="3 4"
points="40,182 120,170 200,158 280,148 360,138 440,128 520,118 580,108"/>
<!-- Mira's actual weight -->
<polygon fill="url(#growthArea)" points="40,200 40,178 130,170 220,158 310,142 400,124 490,108 580,90 580,200"/>
<polyline fill="none" stroke="#9ec79e" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"
points="40,178 130,170 220,158 310,142 400,124 490,108 580,90"/>
<!-- Markers -->
<g>
<circle cx="40" cy="178" r="3.5" fill="#9ec79e"/>
<circle cx="130" cy="170" r="3.5" fill="#9ec79e"/>
<circle cx="220" cy="158" r="3.5" fill="#9ec79e"/>
<circle cx="310" cy="142" r="3.5" fill="#9ec79e"/>
<circle cx="400" cy="124" r="3.5" fill="#9ec79e"/>
<circle cx="490" cy="108" r="3.5" fill="#9ec79e"/>
<circle cx="580" cy="90" r="5" fill="#9ec79e" stroke="#fff" stroke-width="2"/>
</g>
<!-- X labels -->
<g font-family="Inter,sans-serif" font-size="10" fill="#8a7e72" text-anchor="middle">
<text x="40" y="215">birth</text>
<text x="130" y="215">wk 1</text>
<text x="220" y="215">wk 2</text>
<text x="310" y="215">wk 3</text>
<text x="400" y="215">wk 4</text>
<text x="490" y="215">wk 5</text>
<text x="580" y="215" fill="#2a2520" font-weight="600">today</text>
</g>
<!-- "now" callout -->
<text x="572" y="80" text-anchor="end" font-family="Caveat,cursive" font-size="20" fill="#9ec79e" transform="rotate(-3 572 80)">on track ✓</text>
<!-- Y axis labels -->
<g font-family="Geist Mono,monospace" font-size="9.5" fill="#8a7e72">
<text x="6" y="80">5.0</text>
<text x="6" y="120">4.5</text>
<text x="6" y="160">4.0</text>
<text x="6" y="200">3.5</text>
</g>
</svg>
<div class="growth-stats">
<div class="stat">
<div class="lbl">Weight today</div>
<div class="v">4.84 kg</div>
<div class="pctile">62nd percentile · WHO</div>
</div>
<div class="stat">
<div class="lbl">Length</div>
<div class="v">53.4 cm</div>
<div class="pctile">58th percentile · WHO</div>
</div>
<div class="stat">
<div class="lbl">Gain · 7 days</div>
<div class="v">+218 g</div>
<div class="pctile">healthy range (150–280g)</div>
</div>
</div>
</div>
</section>
<!-- ─────── 4. Today's reminders ─────── -->
<section class="block">
<div class="block-head">
<h2><em>iv.</em>Today's gentle reminders</h2>
<span class="meta">4 things, no alarms</span>
</div>
<div class="reminders">
<div class="rem-card feed" data-rem="feed">
<div class="l-row"><span class="glyph">◍</span> Next feed</div>
<div class="when">in ~38 min</div>
<div class="desc">Last feed at 2:00 PM (100 ml bottle). Suggested 100–110 ml.</div>
</div>
<div class="rem-card nap" data-rem="nap">
<div class="l-row"><span class="glyph">☾</span> Next likely nap</div>
<div class="when">3:30 — 5:00 PM</div>
<div class="desc">She tends to fade ~90 min after morning rouse. Recent pattern.</div>
</div>
<div class="rem-card appt" data-rem="appt">
<div class="l-row"><span class="glyph">✓</span> Doctor visit</div>
<div class="when">Friday 10:30 AM</div>
<div class="desc">1-month checkup with Dr. Wei. We'll bring the weight log.</div>
</div>
<div class="rem-card bath" data-rem="bath">
<div class="l-row"><span class="glyph">~</span> Bath night</div>
<div class="when">tonight, 6:30 PM</div>
<div class="desc">Every other day · grandma's lullaby works.</div>
</div>
</div>
</section>
<!-- ─────── 5. AI Insight ─────── -->
<section class="block">
<div class="block-head">
<h2><em>v.</em>What we noticed this week</h2>
<span class="meta"><span class="src">Quiver</span>· soft patterns</span>
</div>
<div class="card insight-card">
<div class="badge">
<span class="glyph">✦</span>
Quiver insights · weekly
</div>
<h3>She's settling into a <em>longer-stretch</em> sleep at night.</h3>
<p>Five small patterns matched up this week. None demand action — they just paint a picture.</p>
<div class="insight-list">
<div class="row"><span class="check">✓</span><span class="text"><strong>Longer first stretch:</strong> first sleep block grew from 2h 14m → 3h 12m on average. <span class="hand">good for you both</span></span></div>
<div class="row"><span class="check">✓</span><span class="text"><strong>Feed-then-sleep latency dropped to 14 min</strong> (was 28 min in week 2). She's calmer post-feed.</span></div>
<div class="row"><span class="check">✓</span><span class="text"><strong>Bottle preference stabilized</strong> on the slow-flow Dr. Brown's after the size-2 trial. No spit-up since Monday.</span></div>
<div class="row"><span class="check">✓</span><span class="text"><strong>62nd percentile</strong> on weight, up from 58th — within healthy range. Length is steady at 58th.</span></div>
<div class="row"><span class="check">✓</span><span class="text"><strong>Diaper count averaged 8 per day</strong> (target ≥6). Hydration is comfortable.</span></div>
</div>
</div>
</section>
<!-- ─────── 6. Photo memory ─────── -->
<section class="block">
<div class="block-head">
<h2><em>vi.</em>This week, in pictures</h2>
<span class="meta"><span class="src">iCloud Photos</span>· auto-curated</span>
</div>
<div class="photo-strip">
<div class="photo">
<div class="day">Mon</div>
<div class="cap">first finger-grip</div>
</div>
<div class="photo">
<div class="day">Wed</div>
<div class="cap">sun nap on the porch</div>
</div>
<div class="photo">
<div class="day">Thu</div>
<div class="cap">grandma met her</div>
</div>
<div class="photo">
<div class="day">Sat</div>
<div class="cap">first real smile (we think)</div>
</div>
</div>
</section>
<div class="closing">
"What you measure, you remember."<br/>
<span class="author">— quiver, a quiet panel · auto-refreshes daily at 6:30 AM</span>
</div>
</main>
<div class="toast-box" id="toastBox"></div>
<script>
(() => {
// Toast
const toastBox = document.getElementById('toastBox');
function toast(opts) {
const { kind = '', title = '', body = '' } = (typeof opts === 'string') ? { body: opts } : opts;
const node = document.createElement('div');
node.className = `toast ${kind}`;
node.innerHTML = `<div class="t-title">${title}</div><div class="t-body">${body}</div>`;
toastBox.appendChild(node);
setTimeout(() => {
node.classList.add('fade');
setTimeout(() => node.remove(), 280);
}, 4200);
}
// Reminders
const remCopy = {
feed: { kind: '', title: 'Logged a feed', body: 'Recorded · <strong>110 ml at 3:10 PM</strong> bottle. Updated next-feed estimate to 6:15 PM.' },
nap: { kind: 'lavender', title: 'Nap window noted', body: 'We\'ll send a quiet ping at <strong>3:25 PM</strong> if she\'s still awake.' },
appt: { kind: 'sage', title: 'Friday checkup', body: 'Calendar event opened · 1-month checkup with Dr. Wei · 10:30 AM.<br/>We\'ll auto-prepare the weight log.' },
bath: { kind: '', title: 'Bath time saved', body: 'Tonight at 6:30 PM. We\'ll dim the smart lights at 6:25.' }
};
document.querySelectorAll('[data-rem]').forEach(r => {
r.addEventListener('click', () => {
const k = r.dataset.rem;
if (remCopy[k]) toast(remCopy[k]);
});
});
// Log a feed (header button)
document.getElementById('logBtn').addEventListener('click', () => {
toast({ kind: 'sage', title: 'New feed logged', body: '<strong>110 ml</strong> bottle · 3:10 PM<br/>Avg gain trend: +218 g this week, on track.' });
});
// Photos
document.querySelectorAll('.photo').forEach(p => {
p.addEventListener('click', () => {
const cap = p.querySelector('.cap').textContent;
toast({ kind: 'lavender', title: 'Photo memory', body: `"${cap}" · saved to your Mira album.` });
});
});
// Feed marker click (open detail)
document.querySelectorAll('.feed-marker').forEach(m => {
m.addEventListener('click', e => {
const tip = m.querySelector('.feed-tip');
if (tip) toast({ kind: '', title: 'Feed entry', body: tip.textContent });
});
});
})();
</script>
</body>
</html>
{
"title": "Quiver Live · Personal Portfolio",
"slug": "quiver-live-portfolio",
"pinned": true,
"preview": {
"type": "html",
"entry": "index.html"
},
"document": {
"format": "html_template_v1",
"templatePath": "template.html",
"generatedPreviewPath": "index.html",
"dataPath": "data.json",
"dataJson": {}
}
}
{
"summary": {
"brand": "quiver",
"pro": "Live",
"marketState": "NYSE Open · 14:32:18 EST",
"lastSync": "14:32:18 EST",
"totalLabel": "Total portfolio value · USD",
"accountsLabel": "All accounts (Robinhood + IBKR)",
"totalValue": "$284,521.64",
"todayPnl": "+$3,182.40",
"todayPct": "+1.13%",
"todayDirClass": "up",
"todayArrow": "▲",
"allTimePnl": "+$84,521.64",
"allTimePct": "+42.3%",
"allTimeDirClass": "up",
"allTimeArrow": "▲",
"alphaLabel": "vs S&P · YTD",
"alpha": "+18.7pp",
"areaPoints": "10,150 10,128 60,124 110,116 160,118 210,108 260,114 310,98 360,90 410,96 460,80 510,72 560,76 610,60 660,54 710,46 760,40 810,32 860,28 870,28 870,150",
"linePoints": "10,128 60,124 110,116 160,118 210,108 260,114 310,98 360,90 410,96 460,80 510,72 560,76 610,60 660,54 710,46 760,40 810,32 860,28",
"markerX": "860",
"markerY": "28",
"axisTicks": [
{ "label": "May '24" },
{ "label": "Jul" },
{ "label": "Sep" },
{ "label": "Nov" },
{ "label": "Jan '25" },
{ "label": "Mar" },
{ "label": "Today" }
]
},
"kpis": [
{
"label": "Today's P&L",
"value": "+$3,182.40",
"valueClass": "up",
"sub": "+1.13% · 8 winners / 2 losers",
"subClass": "up"
},
{
"label": "Buying power",
"value": "$12,438.21",
"valueClass": "",
"sub": "Cash + 2× margin available",
"subClass": ""
},
{
"label": "Best position · NVDA",
"value": "+187.4%",
"valueClass": "up",
"sub": "Avg cost $321.55 · Now $924.31",
"subClass": "up"
},
{
"label": "Alpha vs S&P 500 · YTD",
"value": "+18.7pp",
"valueClass": "alpha",
"sub": "Sharpe 1.42 · Beta 1.18 · Max DD −9.4%",
"subClass": ""
}
],
"featured": {
"iconText": "N",
"iconBg": "linear-gradient(135deg,#76b900,#4d8400)",
"iconFg": "#0a1502",
"symbol": "NVDA",
"name": "NVIDIA Corporation",
"sector": "Semiconductors · Mkt cap $2.27T",
"exchangeName": "NASDAQ",
"exchangeUrl": "https://www.nasdaq.com/market-activity/stocks/nvda",
"yahooUrl": "https://finance.yahoo.com/quote/NVDA",
"tradingViewUrl": "https://www.tradingview.com/symbols/NASDAQ-NVDA/",
"price": "$924.31",
"delta": "▲ +$21.74 +2.41% today",
"dirClass": "up",
"stats": [
{ "label": "Open", "value": "906.80" },
{ "label": "High", "value": "929.18" },
{ "label": "Low", "value": "902.44" },
{ "label": "Volume", "value": "38.42M" },
{ "label": "P/E", "value": "62.4" },
{ "label": "52W range", "value": "412 — 974" }
],
"areaPoints": "40,260 40,220 80,210 120,200 160,194 200,184 240,180 280,170 320,160 360,150 400,138 440,128 480,120 520,108 560,100 600,86 640,76 680,64 720,56 760,48 800,40 840,30 850,30 850,260",
"linePoints": "40,220 80,210 120,200 160,194 200,184 240,180 280,170 320,160 360,150 400,138 440,128 480,120 520,108 560,100 600,86 640,76 680,64 720,56 760,48 800,40 840,30",
"priceY": "30",
"priceLabelY": "21",
"priceLabelTextY": "34",
"priceLabelText": "924.31"
},
"ai": {
"tag": "Quiver AI · Signal #2417",
"headlineAccent": "Add to NVDA",
"headlineRest": "on Blackwell ramp + AI capex inflection.",
"sub": "Three converging catalysts and resilient channel checks. Conviction has stepped up since last week's hyperscaler guidance.",
"conviction": "87%",
"convictionWidth": "87%",
"targetLabel": "12-mo target",
"target": "$1,050",
"upside": "+13.6%",
"size": "+12 sh",
"sizeSub": "≈ $11,092 · 4.2% port",
"catalysts": [
{
"n": "01",
"title": "Blackwell shipments accelerating",
"text": "Foxconn and Wiwynn guidance both implied an Aug volume step-up. Backlog visibility extends through Q1 '26."
},
{
"n": "02",
"title": "Hyperscaler capex revised up",
"text": "MSFT, META, AMZN combined FY '25 capex now +34% YoY. AI infra is the binding allocation."
},
{
"n": "03",
"title": "Sentiment de-risked",
"text": "Short interest at 6-month high but RSI cooling from overbought. Setup mirrors the May '24 base before the +47% leg."
}
]
},
"holdingsPanel": {
"title": "Holdings · 9 positions",
"subtitle": "Live · last quote 14:32:18 EST"
},
"holdings": [
{
"symbol": "NVDA",
"companyName": "NVIDIA Corporation",
"icon": "N",
"iconBg": "linear-gradient(135deg,#76b900,#3d6800)",
"iconFg": "#0a1502",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/nvda",
"shares": "100",
"avgCost": "321.55",
"last": "924.31",
"marketValue": "$92,431",
"pnl": "+$60,276",
"pnlPct": "+187.4%",
"dirClass": "up",
"alloc": "32.5%",
"allocWidth": "32%"
},
{
"symbol": "AAPL",
"companyName": "Apple Inc.",
"icon": "A",
"iconBg": "linear-gradient(135deg,#0c0c0c,#3a3a3a)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/aapl",
"shares": "220",
"avgCost": "152.10",
"last": "228.74",
"marketValue": "$50,323",
"pnl": "+$16,861",
"pnlPct": "+50.4%",
"dirClass": "up",
"alloc": "17.7%",
"allocWidth": "18%"
},
{
"symbol": "MSFT",
"companyName": "Microsoft Corp.",
"icon": "M",
"iconBg": "linear-gradient(135deg,#0078d4,#005a9e)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/msft",
"shares": "85",
"avgCost": "328.40",
"last": "438.12",
"marketValue": "$37,240",
"pnl": "+$9,326",
"pnlPct": "+33.4%",
"dirClass": "up",
"alloc": "13.1%",
"allocWidth": "13%"
},
{
"symbol": "META",
"companyName": "Meta Platforms",
"icon": "M",
"iconBg": "linear-gradient(135deg,#1877f2,#0c5fc3)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/meta",
"shares": "55",
"avgCost": "312.20",
"last": "596.18",
"marketValue": "$32,790",
"pnl": "+$15,619",
"pnlPct": "+91.0%",
"dirClass": "up",
"alloc": "11.5%",
"allocWidth": "11%"
},
{
"symbol": "GOOGL",
"companyName": "Alphabet Class A",
"icon": "G",
"iconBg": "linear-gradient(135deg,#4285f4,#34a853)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/googl",
"shares": "160",
"avgCost": "128.90",
"last": "186.40",
"marketValue": "$29,824",
"pnl": "+$9,200",
"pnlPct": "+44.6%",
"dirClass": "up",
"alloc": "10.5%",
"allocWidth": "10%"
},
{
"symbol": "AMZN",
"companyName": "Amazon.com Inc.",
"icon": "A",
"iconBg": "linear-gradient(135deg,#ff9900,#cc7a00)",
"iconFg": "#1a0e00",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/amzn",
"shares": "90",
"avgCost": "142.50",
"last": "218.94",
"marketValue": "$19,705",
"pnl": "+$6,880",
"pnlPct": "+53.6%",
"dirClass": "up",
"alloc": "6.9%",
"allocWidth": "7%"
},
{
"symbol": "TSLA",
"companyName": "Tesla, Inc.",
"icon": "T",
"iconBg": "linear-gradient(135deg,#cc0000,#660000)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/tsla",
"shares": "42",
"avgCost": "288.40",
"last": "312.55",
"marketValue": "$13,127",
"pnl": "+$1,015",
"pnlPct": "+8.4%",
"dirClass": "up",
"alloc": "4.6%",
"allocWidth": "5%"
},
{
"symbol": "AMD",
"companyName": "Advanced Micro Devices",
"icon": "A",
"iconBg": "linear-gradient(135deg,#ed1c24,#7a0000)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/amd",
"shares": "52",
"avgCost": "158.90",
"last": "142.06",
"marketValue": "$7,387",
"pnl": "−$876",
"pnlPct": "−10.6%",
"dirClass": "down",
"alloc": "2.6%",
"allocWidth": "3%"
},
{
"symbol": "USD",
"companyName": "Settled cash",
"icon": "$",
"iconBg": "linear-gradient(135deg,#facc15,#a17400)",
"iconFg": "#1a1100",
"exchName": "—",
"exchUrl": "#",
"shares": "—",
"avgCost": "—",
"last": "1.00",
"marketValue": "$1,694",
"pnl": "—",
"pnlPct": "—",
"dirClass": "",
"alloc": "0.6%",
"allocWidth": "1%"
}
],
"watchlistPanel": {
"title": "Watchlist · curated",
"subtitle": "12 tickers · 14:32 EST"
},
"watchlist": [
{
"symbol": "PLTR",
"companyName": "Palantir",
"price": "$58.42",
"pct": "▲ 4.81%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,28 20,30 40,26 60,24 80,28 100,22 120,18 140,20 160,14 180,10 200,6",
"volume": "4.21M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/pltr"
},
{
"symbol": "SHOP",
"companyName": "Shopify",
"price": "$112.84",
"pct": "▲ 2.13%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,26 20,28 40,22 60,24 80,20 100,22 120,18 140,16 160,12 180,14 200,10",
"volume": "6.84M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:SHOP"
},
{
"symbol": "TSM",
"companyName": "TSMC",
"price": "$208.77",
"pct": "▲ 1.62%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,24 20,22 40,26 60,20 80,18 100,20 120,16 140,14 160,12 180,10 200,8",
"volume": "12.4M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:TSM"
},
{
"symbol": "COIN",
"companyName": "Coinbase",
"price": "$236.10",
"pct": "▲ 3.48%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,28 20,30 40,26 60,28 80,22 100,18 120,20 140,14 160,10 180,12 200,6",
"volume": "8.92M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/coin"
},
{
"symbol": "CRWD",
"companyName": "CrowdStrike",
"price": "$352.09",
"pct": "▼ 1.24%",
"dirClass": "down",
"sparkColor": "#ff4f6d",
"sparkPoints": "0,14 20,12 40,16 60,14 80,18 100,20 120,18 140,22 160,24 180,28 200,30",
"volume": "3.24M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/crwd"
},
{
"symbol": "SMCI",
"companyName": "Super Micro",
"price": "$48.93",
"pct": "▲ 5.62%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,30 20,32 40,28 60,30 80,26 100,20 120,16 140,18 160,12 180,8 200,4",
"volume": "18.2M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/smci"
},
{
"symbol": "UBER",
"companyName": "Uber Technologies",
"price": "$72.41",
"pct": "▲ 1.18%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,24 20,26 40,22 60,20 80,22 100,18 120,16 140,18 160,14 180,12 200,12",
"volume": "7.42M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:UBER"
},
{
"symbol": "NFLX",
"companyName": "Netflix",
"price": "$728.50",
"pct": "▼ 0.66%",
"dirClass": "down",
"sparkColor": "#ff4f6d",
"sparkPoints": "0,16 20,14 40,18 60,16 80,18 100,16 120,20 140,18 160,22 180,20 200,24",
"volume": "2.86M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/nflx"
}
],
"newsPanel": {
"title": "Live signals & news",
"subtitle": "Filtered to your book"
},
"news": [
{
"time": "14:31",
"source": "AI · Quiver Research",
"headline": "▲ Hyperscaler capex tracker rolls forward — MSFT + META combined FY '25 guide now +34% YoY, reinforcing NVDA/SMCI thesis.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "14:18",
"source": "Bloomberg",
"headline": "Tesla cuts Model Y prices in China by 6% as BYD pressure intensifies in Q2 sales window.",
"impactLabel": "High risk",
"impactClass": "high"
},
{
"time": "13:54",
"source": "Reuters",
"headline": "FOMC minutes signal one more cut probable in Sept — yields fall 9 bps on the long end.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "13:30",
"source": "AI · Earnings whisper",
"headline": "CRWD whisper revised −2.1% after channel checks show enterprise renewal slippage.",
"impactLabel": "Watch",
"impactClass": "med"
},
{
"time": "12:48",
"source": "CNBC",
"headline": "Apple confirms Vision Pro 2 component orders for Q4 — 1.4M unit run rate implied.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "11:22",
"source": "AI · Anomaly detector",
"headline": "▲ SMCI dark-pool prints up 220% vs 30-day baseline — likely block accumulation.",
"impactLabel": "Watch",
"impactClass": "med"
}
],
"footer": {
"live": "Live data — refresh on demand",
"source": "Quotes via IEX Cloud · Robinhood + IBKR linked",
"version": "Quiver Live · v1.4.0"
}
}
{
"generatedAt": "2026-05-06T19:32:18.000Z",
"generatedBy": "agent",
"notes": "Personal trading portfolio dashboard — total value, today's P&L, featured ticker chart, holdings table, watchlist, news/signals, and an AI-generated investment recommendation. Designed to be refreshable on demand: price/holdings/news fields can be rewritten in data.json without re-authoring the template. All numbers, percentages, and SVG polyline points are pre-formatted into display-ready strings so the template stays free of formatting/expression logic; direction is encoded via an explicit dirClass field. External links (exchange, Yahoo, TradingView, per-row exchange page) are bound as plain attribute interpolations — no inline scripting. Refresh runner should write a fresh data.json that matches the template's binding shape; daemon validates against bounded JSON limits and re-derives index.html from template.html + data.json. No credentials, OAuth tokens, cookies, raw provider responses, or HTTP envelopes are persisted; forbidden keys (raw, payload, body, headers, cookie, authorization, token, secret, credential, password) are not present in data.json.",
"sources": [
{
"label": "Brokerage holdings (mocked)",
"type": "connector"
},
{
"label": "Quotes (mocked)",
"type": "connector"
},
{
"label": "AI recommendation",
"type": "derived"
}
]
}
Live Artifact Schema Reference
Live artifacts are stored as daemon-owned project files under .live-artifacts/<artifactId>/. Agents author the source files, then register them through daemon tooling. The daemon assigns IDs, project scope, timestamps, run scope, and refresh status.
Source files
| File | Owner | Purpose |
|---|---|---|
artifact.json | agent-authored input, daemon-validated | Artifact metadata, preview settings, document metadata, and source descriptors. Must not contain daemon-owned fields. |
template.html | agent-authored | html_template_v1 template used to render the preview. |
data.json | agent-authored then refresh-runner-updated | Canonical preview data. API document.dataJson is only a derived cache. |
provenance.json | agent-authored then refresh-runner-updated | Source summary and generation notes. |
index.html | daemon-derived | Generated preview output. Do not treat as source of truth. |
Create/update input
artifact.json should match LiveArtifactCreateInput or LiveArtifactUpdateInput from packages/contracts/src/api/live-artifacts.ts.
Allowed agent-owned top-level fields:
titleslugsessionIdpinnedstatuspreviewdocument
Daemon-owned fields are rejected in agent input:
idprojectIdcreatedAtupdatedAtcreatedByRunIdschemaVersionrefreshStatuslastRefreshedAt
HTML document contract
MVP documents use html_template_v1:
{
"format": "html_template_v1",
"templatePath": "template.html",
"generatedPreviewPath": "index.html",
"dataPath": "data.json",
"dataJson": {}
}template.html + data.json is rendered by the daemon into index.html and the preview route.
Binding rules
- Use escaped interpolation:
{{data.path.to.value}}. - Paths must start with
dataand use dot-separated keys; numeric array indexes are allowed as path segments. - Supported structural directive:
data-od-repeat="item in data.items"for one-level array repeats. - Nested repeats, conditionals, filters, helper functions, partials, and expression evaluation are not supported.
- Raw HTML insertion is forbidden: no triple braces, ampersand interpolation,
data-od-html,data-od-raw, or equivalent. - Interpolation in text and ordinary attributes is HTML-escaped by default.
- Do not interpolate inside tag names, attribute names, comments,
<script>,<style>,<iframe srcdoc>, event-handler attributes, or unsupported URL-bearing attributes.
Bounded JSON limits
All persisted JSON values must fit the shared bounded JSON envelope:
| Limit | Value |
|---|---|
| Maximum object/array depth | 8 |
| Maximum keys per object | 100 |
| Maximum array length | 500 |
| Maximum string length | 16 KiB |
| Maximum serialized JSON size | 256 KiB |
Forbidden keys anywhere in persisted JSON include raw, rawResponse, payload, body, headers, cookie, authorization, token, secret, credential, and password.
Minimal static artifact input
{
"title": "Release Status",
"preview": { "type": "html", "entry": "index.html" },
"document": {
"format": "html_template_v1",
"templatePath": "template.html",
"generatedPreviewPath": "index.html",
"dataPath": "data.json",
"dataJson": {
"summary": {
"title": "Release Status",
"status": "On track"
}
}
}
}Refresh Contract Reference
Refresh updates live artifact data without redesigning the presentation. The refresh runner updates data.json, provenance, and audit history; it does not allow arbitrary template rewrites.
Refreshable source metadata
Refreshable documents use sourceJson:
{
"type": "connector_tool",
"toolName": "list_releases",
"input": {},
"connector": {
"connectorId": "github",
"accountLabel": "example/org",
"toolName": "list_releases"
},
"outputMapping": {
"dataPaths": [{ "from": "items", "to": "releases" }],
"transform": "compact_table"
},
"refreshPermission": "manual_refresh_granted_for_read_only"
}Supported source types:
local_filedaemon_toolconnector_tool
Supported output transforms:
identitycompact_tablemetric_summary
Source execution model
refreshPermissionis retained for backward compatibility with older artifacts, but the refresh runner does not require a separate connector approval step.- If a safe source descriptor exists, manual refresh executes it through daemon-owned local or connector wrappers.
- Write, destructive, unknown, disabled, unconnected, or schema-drifted connector tools should not be authored as refresh sources.
Connector-backed refresh
Connector-backed refresh sources use the same connector execution service as agent-initiated connector calls. Do not call provider APIs directly from refresh logic or from skill-authored scripts.
Before creating a connector-backed refresh source:
1. List connectors with "$OD_NODE_BIN" "$OD_BIN" tools connectors list --format compact. 2. If the user named a connector/source and it is connected, select that connector directly instead of asking where the source is. Then select a tool whose safety is read + auto and whose catalog metadata marks it refresh-eligible. 3. Execute once with "$OD_NODE_BIN" "$OD_BIN" tools connectors execute --connector <id> --tool <name> --input input.json to produce compact normalized preview data. 4. Store only non-sensitive connector references, the bounded input object, output mapping, and compatibility refreshPermission in sourceJson.
On each refresh, the daemon must re-check connector status, account label, allowlist membership, input schema, and output protection. If any check fails or output protection rejects the result, refresh fails all-or-nothing and preserves the previous valid preview.
Persisted connector refresh metadata may include connectorId, toolName, non-sensitive accountLabel, bounded input, outputMapping, and compatibility refreshPermission. It must not include credentials, auth/session material, raw provider envelopes, or unbounded provider responses.
Commit behavior
Refresh is all-or-nothing:
1. Acquire one active refresh lock per artifact. 2. Execute each refreshable source with timeouts and current safety checks. 3. Build candidate data.json, provenance, and preview. 4. Validate all candidates with the same schemas used for create/update. 5. Commit only if every refreshable source succeeds. 6. Preserve the previous valid preview if any step fails.
Refresh IDs must be monotonic so stale runs cannot overwrite newer committed data.
Audit storage
- Append compact records to
refreshes.jsonl. - Successful refresh snapshots live under
snapshots/<refreshId>/and may includedata.jsonand provenance. - Failed refreshes are summarized in
refreshes.jsonlwithout leaking raw provider output or credentials. - On daemon startup, stale running refreshes should be marked failed or timed out while preserving the last valid preview.