diff --git a/README.md b/README.md
index 8539bfc..7c335e7 100644
--- a/README.md
+++ b/README.md
@@ -127,9 +127,25 @@ in the official API. Avoid having native and Frame Flow playback compete.
## Templates / 图片
-Six JSON layout templates: daily-dashboard, weather, calendar, quote, photo-text,
-system-alert. color and grayscale profiles. Photo data accepts photo_id = local asset
-ID. Mock weather is clearly labeled; no unreliable lunar date is fabricated.
+Seven JSON layout templates: daily-dashboard, weather, calendar, quote, photo-text,
+daily-poetry and system-alert, with color and grayscale profiles. Every template
+renders an image, including templates that contain only text.
+
+使用流程:选择模板 → 编辑数据 → 生成预览 → 显示到画框。生成预览只创建本地图片;
+保存默认数据只保存配置,两者都不会更新设备。自动化模板任务每次运行重新生成图片。
+
+- **照片与文字 / Photo + Text**:所选照片铺满画面,底部叠加文字。
+- **家庭早报 / Daily Dashboard**:日期、天气、事项、一句话与所选照片区域。
+- **天气 / Weather**:当前天气与三天预报;天气 JSON 留空时使用配置的 Provider。
+- **日历、每日诗词、一句话、系统告警**:将相应信息排版为图片,不自动选择图库照片。
+
+Photos use a local asset ID (`photo_id`); without a selected photo, a placeholder
+is rendered. Configure Open-Meteo and location coordinates in **Settings → Weather
+source** for real weather. Forecasts are fetched on demand and cached for ten minutes;
+unavailable/stale data is labeled explicitly. Weather remains static on the frame
+until the next render and display, so use an Automation job for regular updates.
+Official KoKonna widgets are a separate integration, not these local templates.
+No unreliable lunar date is fabricated.
Contain preserves composition with margins; cover crops centrally; smart is an
edge-saliency heuristic. Always inspect preview; face-aware guarantees are not claimed.
Cloud uploads activate immediately, so gallery-only means **local staging**.
diff --git a/pigway_frame_flow/web/app.js b/pigway_frame_flow/web/app.js
index 53fbe91..f3381fa 100644
--- a/pigway_frame_flow/web/app.js
+++ b/pigway_frame_flow/web/app.js
@@ -80,7 +80,7 @@ const views={
async dashboard(epoch){const d=await api('/api/v1/devices/'+state.device);if(epoch!==state.epoch)return;const s=d.slideshow;$('#main').innerHTML=notice()+`${d.power.battery_percent!==null&&d.power.battery_percent<20?`
`);bind('acknowledge',async()=>{await api('/api/v1/operations/'+o.operation_id+'/reconcile',{method:'POST',body:{acknowledge:true}});$('#modal').close();await navigate();});}));},
diff --git a/pigway_frame_flow/web/i18n.js b/pigway_frame_flow/web/i18n.js
index 798b94d..9ebeef8 100644
--- a/pigway_frame_flow/web/i18n.js
+++ b/pigway_frame_flow/web/i18n.js
@@ -43,3 +43,18 @@ Object.assign(window.MESSAGES.zh,{previousPage:'上一页',nextPage:'下一页'}
Object.assign(window.MESSAGES.zh,{fitScreen:'适应屏幕',actualSize:'原始尺寸(1:1)'});
Object.assign(window.MESSAGES.en,{fitScreen:'Fit to screen',actualSize:'Actual size (1:1)'});
+
+Object.assign(window.MESSAGES.zh,{
+ templateHint:'模板会把文字、天气和所选照片排版成一张图片。画框显示的是这张图片,不是可实时更新的网页。',
+ templateSteps:'① 编辑内容 → ② 生成预览(仅在本地生成)→ ③ 显示到画框。保存默认数据只保存设置,不改变画框。需要定时更新时,请在自动化中新建模板任务。',
+ templatePhotoHelp:'此模板支持照片:选择上传照片或上传新照片。照片与文字一起生成;不选择照片时使用占位区域。',
+ templateContentHelp:'此模板显示排版后的信息内容,不会自动选取图库照片。天气与日期在每次生成时更新,显示后保持静态,直到再次生成并显示。',
+ templateWeatherHelp:'天气留空时自动读取「设置 → 天气来源」。仅在需要手动覆盖时填写 JSON。'
+});
+Object.assign(window.MESSAGES.en,{
+ templateHint:'Templates arrange text, weather and selected photos into one image. The frame displays that image, rather than a live webpage.',
+ templateSteps:'1. Edit content → 2. Generate preview (local only) → 3. Display on frame. Saving defaults only saves settings. Create a template job in Automation for scheduled updates.',
+ templatePhotoHelp:'This template supports photos: choose an uploaded photo or upload a new one. The photo and text render together; without a photo, a placeholder is shown.',
+ templateContentHelp:'This template displays formatted information and does not automatically choose gallery photos. Weather and dates update when rendering; the displayed image stays static until rendered and displayed again.',
+ templateWeatherHelp:'Leave weather blank to use Settings → Weather source automatically. Enter JSON only to override it manually.'
+});
diff --git a/pigway_frame_flow/web/style.css b/pigway_frame_flow/web/style.css
index 7c83dd5..a0e0a7c 100644
--- a/pigway_frame_flow/web/style.css
+++ b/pigway_frame_flow/web/style.css
@@ -67,8 +67,8 @@ dialog.image-viewer h2.image-viewer-name{font-size:16px;min-width:0;flex:1}
.image-viewer-actions{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin:0}
.image-viewer-actions button{white-space:nowrap}
.image-viewer-actions button[aria-pressed="true"]{background:var(--accent);color:var(--panel)}
-.image-viewer-viewport{display:flex;align-items:center;justify-content:center;flex:1;min-height:0;min-width:0;overflow:hidden;background:var(--soft);border-radius:12px}
-.image-viewer-viewport img{display:block;width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;margin:0}
+.image-viewer-viewport{display:flex;align-items:center;justify-content:center;flex:1;min-height:0;min-width:0;overflow:hidden;background:var(--soft);border-radius:0}
+.image-viewer-viewport img{display:block;width:100%;height:100%;max-width:100%;max-height:100%;object-fit:contain;margin:0;border-radius:0}
.image-viewer-viewport.actual-size{display:flex;align-items:flex-start;justify-content:flex-start;overflow:auto}
.image-viewer-viewport.actual-size img{flex:0 0 auto;width:auto;height:auto;max-width:none;max-height:none;margin:auto}
@media(max-width:600px){dialog.image-viewer{width:calc(100vw - 16px);height:calc(100dvh - 16px);max-height:calc(100dvh - 16px);padding:12px}}