Add shared ref-17 Studio cover template trial
studio CI and deployment / verify-and-deploy (push) Successful in 3m58s

This commit is contained in:
2026-10-26 19:59:51 +08:00
parent f8fde443f5
commit c3765e7a07
9 changed files with 242 additions and 8 deletions
+33
View File
@@ -0,0 +1,33 @@
import type {Idea,Slide} from '../content/ideas';
type Template=NonNullable<Idea['template']>;
export function Ref17Cover({template,slide,masthead=template.masthead,guides=false}:{template:Template;slide:Slide;masthead?:string[];guides?:boolean}){
const scale=10,W=template.width*scale,H=template.height*scale;
const slot=(id:string)=>{const s=template.slots.find(s=>s.id===id);if(!s)throw Error('Missing source slot '+id);return s.box.map(v=>v*scale)};
const [mx,my,mw,mh]=slot('I01'),[cx,cy,cw,ch]=slot('I02'),[dx,dy,dw,dh]=slot('D02'),[bx,by,bw,bh]=slot('D03'),[lx,ly,lw,lh]=slot('D01');
const [aX,aY,aW,aH]=slot('T01'),[bX,bY,bW,bH]=slot('T02'),[tX,tY,tW,tH]=slot('T03'),[sX,sY,sW,sH]=slot('T04');
const first=masthead[0]||'STUDY',second=masthead[1]||'Skills',body=slide.body.split('\n').slice(0,3);
const font1=Math.min(aH*1.2,aW/Math.max(1,first.length*.72)),font2=Math.min(bH,bW/Math.max(1,second.length*.59));
const titleSize=Math.min(tH*.72,tW/Math.max(1,Array.from(slide.heading).length*1.06));
const bodySize=Math.min(32,...body.map(s=>sW/Math.max(1,Array.from(s).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:.5),0))));
const accent=slide.accent||'#d8ddd4';
return <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} ${H}`} width={W} height={H} role="img" aria-label={`${slide.label||slide.heading},ref-17 模板封面试版`} data-layout-id={template.id} style={{display:'block',width:'100%',height:'auto'}}>
<title>{slide.label||slide.heading} · 模板试版</title><desc>源自 ref-17 的九个固定构件。图片位使用色块,占位文案可替换,暂无具体工具内容。</desc>
<rect width={W} height={H} fill={template.paper}/>
<g data-slot="I01"><rect x={mx} y={my} width={mw} height={mh} fill={slide.color}/></g>
<g data-slot="I02"><ellipse cx={cx+cw/2} cy={cy+ch/2} rx={cw/2} ry={ch/2} fill={accent}/></g>
<g data-slot="D02" fill="none" stroke={template.ink} strokeWidth="2" opacity=".75"><rect x={dx+1} y={dy+9} width={dw-2} height={dh-18} rx={(dh-18)/2}/><path d={`M${dx+19} ${dy+dh/2}H${dx+dw-19}`}/></g>
<g data-slot="D01"><path d={`M${lx} ${ly+lh/2}H${lx+lw}`} stroke={template.ink} strokeWidth="2"/></g>
<g data-slot="D03"><rect x={bx} y={by} width={bw} height={bh} fill={accent}/></g>
<g fill={template.ink} fontFamily="Georgia, 'Times New Roman', serif" fontWeight="400">
<text data-slot="T01" x={aX} y={aY+aH*.9} fontSize={font1} letterSpacing={-font1*.035}>{first}</text>
<text data-slot="T02" x={bX} y={bY+bH*.76} fontSize={font2} letterSpacing={-font2*.045}>{second}</text>
</g>
<text data-slot="T03" x={tX} y={tY+tH*.8} fill={template.ink} fontFamily="'PingFang SC', 'Microsoft YaHei', sans-serif" fontWeight="600" fontSize={titleSize} letterSpacing={-titleSize*.025}>{slide.heading}</text>
<g data-slot="T04" fill={template.ink} fontFamily="Arial, 'PingFang SC', sans-serif">
<text x={sX} y={sY+32} fontSize="22" letterSpacing="2">{slide.kicker||'TEMPLATE / XX'}</text>
{body.map((line,i)=><text key={i} x={sX} y={sY+135+i*58} fontSize={bodySize}>{line}</text>)}
<text x={sX} y={sY+sH-62} fontSize="19" letterSpacing="1">CONTENT</text><text x={sX} y={sY+sH-26} fontSize="19" letterSpacing="1">PLACEHOLDER</text>
</g>
{guides&&<g aria-hidden="true" fontFamily="Arial,sans-serif">{template.slots.map(s=>{const[x,y,w,h]=s.box.map(v=>v*scale),color=({text:'#2c64d8',image:'#d36b30',decoration:'#08875d'} as Record<string,string>)[s.kind]||'#666';return <g key={s.id}><rect x={x} y={y} width={w} height={h} fill={color} fillOpacity=".04" stroke={color} strokeWidth="2" strokeDasharray="8 5"/><rect x={x} y={y} width="66" height="29" fill={color}/><text x={x+6} y={y+22} fontSize="24" fill="white">{s.id}</text></g>})}</g>}
</svg>;
}
@@ -0,0 +1,29 @@
import {useState} from 'react';
import Head from 'next/head';
import Link from 'next/link';
import {ideaMarkdown,type Idea,type Slide} from '../content/ideas';
import {Ref17Cover} from './Ref17Cover';
export function TemplateTrial({idea}:{idea:Idea}){
const template=idea.template!;
const [slides,setSlides]=useState<Slide[]>(idea.slides),[active,setActive]=useState(0),[masthead,setMasthead]=useState(template.masthead),[guides,setGuides]=useState(false),[notice,setNotice]=useState('');
const selected=slides[active],current={...idea,slides,template:{...template,masthead}};
const update=(patch:Partial<Slide>)=>setSlides(old=>old.map((slide,i)=>i===active?{...slide,...patch}:slide));
const download=(text:string,type:string,name:string)=>{const url=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),500)};
async function exportSvg(){await document.fonts.ready;const svg=document.querySelector(`[data-proof="${selected.key}"] svg`);if(!svg)return;download(new XMLSerializer().serializeToString(svg),'image/svg+xml',`ref-17-${selected.key}.svg`);setNotice('已下载当前封面 SVG,可继续编辑文字与图形。')}
const reset=()=>{setSlides(idea.slides);setMasthead(template.masthead);setGuides(false);setNotice('已恢复初始试版。')};
return <main className="template-trial"><Head><title>{idea.title} · Studio</title><link rel="alternate" type="text/markdown" href={'/studio/api/export/'+idea.slug+'?format=markdown'}/></Head>
<header className="trial-nav"><Link href="/">← 我的创作</Link><span>STUDIO / TEMPLATE PROOF</span><a href={template.tutorialUrl} target="_blank" rel="noreferrer">排版教程 ↗</a></header>
<section className="trial-intro"><div><small>REF-17 · 同一母版 / 四张试版</small><h1>先试封面,再填内容。</h1><p>英文刊头、圆形叠标、左下主图与右侧窄栏保持同一套坐标。三组合集先用主题占位,暂不放入具体 Skills。</p></div><div className="trial-source"><b>01 母版</b><span>02 笔记复习</span><span>03 教学资料</span><span>04 英语学习</span></div></section>
<div className="trial-workspace"><aside className="trial-controls"><h2>试一试</h2><label>当前封面<select value={active} onChange={e=>{setActive(Number(e.target.value));setNotice('')}}>{slides.map((s,i)=><option value={i} key={s.key}>{s.label}</option>)}</select></label>
<fieldset><legend>共享英文刊头</legend><label>第一行<input value={masthead[0]} maxLength={8} onChange={e=>setMasthead([e.target.value.toUpperCase().replace(/[^A-Z ]/g,''),masthead[1]])}/></label><label>第二行<input value={masthead[1]} maxLength={8} onChange={e=>setMasthead([masthead[0],e.target.value.replace(/[^A-Za-z ]/g,'')])}/></label></fieldset>
<label>本张标题 <small>建议 4–6 字</small><input maxLength={6} value={selected.heading} onChange={e=>update({heading:e.target.value})}/></label>
<label>右侧简介 <small>最多三行</small><textarea rows={3} value={selected.body} onChange={e=>update({body:e.target.value.split('\n').slice(0,3).join('\n')})}/></label>
<div className="trial-colors"><label>主图色块<input type="color" value={selected.color} onChange={e=>update({color:e.target.value})}/></label><label>圆形与贴条<input type="color" value={selected.accent} onChange={e=>update({accent:e.target.value})}/></label></div>
<label className="trial-switch"><input type="checkbox" checked={guides} onChange={e=>setGuides(e.target.checked)}/>显示构件边界</label>
<div className="trial-actions"><button className="trial-primary" onClick={exportSvg}>下载当前 SVG</button><button onClick={()=>download(JSON.stringify({version:2,...current},null,2),'application/json',idea.slug+'.json')}>导出当前试版 JSON</button><button onClick={()=>download(ideaMarkdown(current),'text/markdown',idea.slug+'.md')}>导出说明 Markdown</button><button onClick={reset}>恢复初始试版</button></div>
<p className="trial-help">修改仅保留在当前页面,刷新会恢复;可下载 JSON 保存试版。主视觉仍是色块占位。</p><p role="status" className="trial-notice">{notice}</p>
</aside><div><div className="trial-grid">{slides.map((slide,i)=><figure className={'proof-card '+(i===active?'proof-active':'')} key={slide.key} data-proof={slide.key}><button className="proof-select" onClick={()=>setActive(i)} aria-label={'编辑'+slide.label} aria-pressed={i===active}><Ref17Cover template={template} slide={slide} masthead={masthead} guides={guides}/></button><figcaption><b>{slide.label}</b><span>共用 REF-17</span><a href={'/studio/api/export/'+idea.slug+'?format=svg&slide='+i}>初始 SVG ↗</a></figcaption></figure>)}</div>
<details className="trial-reference"><summary>查看原图与构件对应</summary><div className="trial-reference-body"><img src={template.referenceUrl} alt="ref-17 原始参考裁片"/><div><p>保留 {template.width}×{template.height} 的画板比例与九个构件位置;本试版重新绘制可编辑文字与色块,替换了文案和色调。</p><table><thead><tr><th>构件</th><th>用途</th><th>原图坐标</th></tr></thead><tbody>{template.slots.map(s=><tr key={s.id}><td>{s.id}</td><td>{s.role}</td><td>{s.box.join(', ')}</td></tr>)}</tbody></table><a href={template.tutorialUrl+'?agent_mode=True'}>读取教程纯文本 ↗</a></div></div></details>
</div></div>
</main>;
}
+17 -3
View File
@@ -1,3 +1,17 @@
export type Idea = {slug:string; title:string; summary:string; slides:{heading:string;body:string;color:string}[]};
// Add a page here; the page, AI text and export bundle share this source.
export const ideas:Idea[]=[{slug:'first-idea',title:'把一个想法,变成一组画面',summary:'从一句话出发,写成页面,再导出为图文或视频。',slides:[{heading:'先捕捉,再打磨',body:'把今天的灵感写下来。不用急着完整,先留下那个让你心动的瞬间。',color:'#ffe5df'},{heading:'让每一页只讲一件事',body:'一个标题,一段解释,一个值得被记住的画面。',color:'#e2e9e1'},{heading:'选择你的表达方式',body:'导出图文长页,或用 Python 将画面制作成视频。',color:'#eee5f5'}]}];
import ref17 from './templates/ref-17.json';
export type Slide={heading:string;body:string;color:string;key?:string;label?:string;accent?:string;kicker?:string};
export type Idea={slug:string;title:string;summary:string;slides:Slide[];template?:typeof ref17};
// Studio owns creative pages. Wiki source tutorials are consumed as text/image data.
export const ideas:Idea[]=[{
slug:'learning-skills-template',title:'学习资料合集 · 封面模板试版',
summary:'一张空白母版与三张主题占位封面,共用 ref-17 的同一套构件坐标;先试文案、色调和边界,暂不填具体 Skills。',template:ref17,
slides:[
{key:'master',label:'空白母版',heading:'合集标题',body:'副标题占位\n栏目一 · 栏目二\n正文稍后填入',color:'#84918c',accent:'#d8ddd4',kicker:'TEMPLATE / 00'},
{key:'notes',label:'A · 笔记复习',heading:'笔记复习',body:'课程 · 笔记 · 复习\n副标题稍后确定\n正文暂不填入',color:'#7a8b7e',accent:'#d5ddcf',kicker:'COLLECTION / A'},
{key:'teaching',label:'B · 教学资料',heading:'教学资料',body:'题目 · 试卷 · 课件\n副标题稍后确定\n正文暂不填入',color:'#7c8e9b',accent:'#d8e0e4',kicker:'COLLECTION / B'},
{key:'english',label:'C · 英语学习',heading:'英语学习',body:'词汇 · 精读 · 表达\n副标题稍后确定\n正文暂不填入',color:'#9b817a',accent:'#e4d6ce',kicker:'COLLECTION / C'},
],
},{slug:'first-idea',title:'把一个想法,变成一组画面',summary:'从一句话出发,写成页面,再导出为图文或视频。',slides:[{heading:'先捕捉,再打磨',body:'把今天的灵感写下来。不用急着完整,先留下那个让你心动的瞬间。',color:'#ffe5df'},{heading:'让每一页只讲一件事',body:'一个标题,一段解释,一个值得被记住的画面。',color:'#e2e9e1'},{heading:'选择你的表达方式',body:'导出图文长页,或用 Python 将画面制作成视频。',color:'#eee5f5'}]}];
export function ideaMarkdown(idea:Idea){
return '# '+idea.title+'\n\n'+idea.summary+'\n\n'+(idea.template?'## 模板来源与约束\n\n教程:'+idea.template.tutorialUrl+'?agent_mode=True\n\n同一套模板:'+idea.template.id+';源画板 '+idea.template.width+'×'+idea.template.height+'。本阶段仅有占位封面,没有具体 Skills 内容。\n\n```json\n'+JSON.stringify(idea.template,null,2)+'\n```\n\n':'')+idea.slides.map(s=>'## '+(s.label||s.heading)+'\n\n'+s.heading+'\n\n'+s.body+'\n\n'+(idea.template?'主色:'+s.color+';叠圆:'+s.accent+'。\n':'')).join('\n\n');
}
@@ -0,0 +1,116 @@
{
"id": "ref-17",
"name": "英文刊头与圆形叠标及窄栏",
"tutorialUrl": "http://8.211.154.134:18131/explore/7b1b2fc5b9690213bbbe07ac",
"referenceUrl": "http://8.211.154.134:18131/api/media/447db777-d060-4987-b753-15422b0b4919",
"sourceSha256": "121ab8a362c45eee86281ca5dfbbe33577cfcd255579a16c3542530484b9f1dc",
"width": 113,
"height": 174,
"masthead": [
"STUDY",
"Skills"
],
"paper": "#f4f2eb",
"ink": "#25342f",
"slots": [
{
"id": "D01",
"kind": "decoration",
"role": "右栏底线",
"box": [
82,
121,
29,
2
]
},
{
"id": "T01",
"kind": "text",
"role": "左上英文第一行",
"box": [
6,
10,
63,
10
]
},
{
"id": "T02",
"kind": "text",
"role": "左上英文第二行",
"box": [
5,
20,
56,
16
]
},
{
"id": "T03",
"kind": "text",
"role": "中部中文横题",
"box": [
51,
50,
60,
16
]
},
{
"id": "T04",
"kind": "text",
"role": "右侧窄栏正文",
"box": [
82,
67,
29,
54
]
},
{
"id": "I01",
"kind": "image",
"role": "左下主图",
"box": [
5,
68,
73,
101
]
},
{
"id": "I02",
"kind": "image",
"role": "左中叠加圆形",
"box": [
5,
53,
22,
22
]
},
{
"id": "D02",
"kind": "decoration",
"role": "刊头右侧双线",
"box": [
61,
27,
50,
7
]
},
{
"id": "D03",
"kind": "decoration",
"role": "底部横向贴条",
"box": [
58,
167,
54,
6
]
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
import type {NextApiRequest,NextApiResponse} from 'next';
const docs:Record<string,string>={
'/studio/api/export/{slug}': '# 导出创作方案\n\n方法:GET\n路径参数:slug,创作方案的稳定标识。\n查询参数:format=markdown 返回 Markdown;其他值或省略时返回带 version:1 的方案 JSON,带附件文件名。\n示例:GET /studio/api/export/{slug}\n错误:404 方案不存在;405 方法不支持。',
'/studio/api/export/{slug}': '# 导出创作方案\n\n方法:GET\n路径参数:slug,创作方案的稳定标识。\n查询参数:format=markdown 返回 Markdown;format=svg&slide=0 返回模板封面 SVG(slide 为从 0 开始的整数,默认 0);其他值或省略时返回方案 JSON,带附件文件名。普通方案 version:1;模板方案 version:2,包含 template 来源链接、画板比例、原图九个 slots 及 slides 文字和颜色。模板几何共用,图片位为占位色块。\n示例:GET /studio/api/export/{slug}\n错误:400 SVG 模板或 slide 参数无效;404 方案不存在;405 方法不支持。',
'/studio/api/health': '# 创作站健康检查\n\n方法:GET, HEAD\n无业务参数。GET 返回 200 JSON:{"status":"ok","service":"prompt-studio"};HEAD 无正文。\n示例:GET /studio/api/health\n错误:405 方法不支持。',
};
export function serveApiDocumentation(req:NextApiRequest,res:NextApiResponse,path:string){
+17 -2
View File
@@ -1,4 +1,19 @@
import {createElement} from 'react';
import {renderToStaticMarkup} from 'react-dom/server';
import {serveApiDocumentation} from '../../../lib/api-documentation';
import type {NextApiRequest,NextApiResponse} from 'next';
import {ideas} from '../../../content/ideas';
export default function handler(req:NextApiRequest,res:NextApiResponse){if(serveApiDocumentation(req,res,'/studio/api/export/{slug}'))return;if(req.method!=='GET')return res.status(405).end();const idea=ideas.find(i=>i.slug===req.query.slug);if(!idea)return res.status(404).end();if(req.query.format==='markdown'){res.setHeader('Content-Type','text/markdown; charset=utf-8');return res.send('# '+idea.title+'\n\n'+idea.summary+'\n\n'+idea.slides.map(s=>'## '+s.heading+'\n\n'+s.body).join('\n\n'));}res.setHeader('Content-Disposition',`attachment; filename="${idea.slug}.json"`);return res.json({version:1,...idea});}
import {ideas,ideaMarkdown} from '../../../content/ideas';
import {Ref17Cover} from '../../../components/Ref17Cover';
export default function handler(req:NextApiRequest,res:NextApiResponse){
if(serveApiDocumentation(req,res,'/studio/api/export/{slug}'))return;
if(req.method!=='GET')return res.status(405).end();
const idea=ideas.find(i=>i.slug===req.query.slug);if(!idea)return res.status(404).end();
if(req.query.format==='markdown'){res.setHeader('Content-Type','text/markdown; charset=utf-8');return res.send(ideaMarkdown(idea));}
if(req.query.format==='svg'){
const raw=req.query.slide??'0',index=typeof raw==='string'&&/^\d+$/.test(raw)?Number(raw):-1;
if(!idea.template||!idea.slides[index])return res.status(400).json({error:'SVG requires a template and a valid zero-based slide index.'});
res.setHeader('Content-Type','image/svg+xml; charset=utf-8');res.setHeader('Content-Disposition',`inline; filename="${idea.slug}-${index}.svg"`);
return res.send(renderToStaticMarkup(createElement(Ref17Cover,{template:idea.template,slide:idea.slides[index]})));
}
res.setHeader('Content-Disposition',`attachment; filename="${idea.slug}.json"`);return res.json({version:idea.template?2:1,...idea});
}
+2 -1
View File
@@ -1,7 +1,8 @@
import type {GetStaticPaths,GetStaticProps} from 'next';
import Head from 'next/head';
import Link from 'next/link';
import {TemplateTrial} from '../../components/TemplateTrial';
import {ideas,type Idea} from '../../content/ideas';
export default function IdeaPage({idea}:{idea:Idea}){return <main className="editor"><Head><title>{idea.title} · 创作站</title><link rel="alternate" type="text/markdown" href={'/studio/api/export/'+idea.slug+'?format=markdown'}/></Head><header><Link href="/">← 我的创作</Link><a href={'/studio/api/export/'+idea.slug}>导出内容包 ↓</a><a href={'/studio/api/export/'+idea.slug+'?format=markdown'}>Markdown ↓</a><button onClick={()=>window.print()}>打印 / PDF</button></header><h1>{idea.title}</h1><p>{idea.summary}</p><div className="slides">{idea.slides.map((s,i)=><article className="slide" key={i} style={{background:s.color}}><small>PROMPT STUDIO / {String(i+1).padStart(2,'0')}</small><h2>{s.heading}</h2><p>{s.body}</p><footer>记录灵感 · 自由表达</footer></article>)}</div></main>}
export default function IdeaPage({idea}:{idea:Idea}){if(idea.template)return <TemplateTrial idea={idea}/>;return <main className="editor"><Head><title>{idea.title} · 创作站</title><link rel="alternate" type="text/markdown" href={'/studio/api/export/'+idea.slug+'?format=markdown'}/></Head><header><Link href="/">← 我的创作</Link><a href={'/studio/api/export/'+idea.slug}>导出内容包 ↓</a><a href={'/studio/api/export/'+idea.slug+'?format=markdown'}>Markdown ↓</a><button onClick={()=>window.print()}>打印 / PDF</button></header><h1>{idea.title}</h1><p>{idea.summary}</p><div className="slides">{idea.slides.map((s,i)=><article className="slide" key={i} style={{background:s.color}}><small>PROMPT STUDIO / {String(i+1).padStart(2,'0')}</small><h2>{s.heading}</h2><p>{s.body}</p><footer>记录灵感 · 自由表达</footer></article>)}</div></main>}
export const getStaticPaths:GetStaticPaths=async()=>({paths:ideas.map(i=>({params:{slug:i.slug}})),fallback:false});
export const getStaticProps:GetStaticProps=async({params})=>{const idea=ideas.find(i=>i.slug===params?.slug);return idea?{props:{idea}}:{notFound:true}};
+2 -1
View File
@@ -1,3 +1,4 @@
import Link from 'next/link';
import {Ref17Cover} from '../components/Ref17Cover';
import {ideas} from '../content/ideas';
export default function Home(){return <main className="workspace"><aside><b className="logo">PROMPT / 创作</b><p>想法 · 页面 · 作品</p><a href="/">我的创作</a><a href="/studio/api">AI 内容索引 ↗</a></aside><section><small>YOUR CREATIVE WORKSPACE</small><h1>今天,想表达什么?</h1><p>一个页面,装下一个想法。让内容慢慢长成作品。</p><div className="grid">{ideas.map(i=><Link className="card" href={'/ideas/'+i.slug} key={i.slug}><div className="cover">{i.title}</div><h2>{i.title}</h2><p>{i.summary}</p><span>打开创作 →</span></Link>)}</div><p className="hint">新增作品:在 src/content/ideas.ts 添加内容,页面与导出会同步生成。</p></section></main>}
export default function Home(){return <main className="workspace"><aside><b className="logo">PROMPT / 创作</b><p>想法 · 页面 · 作品</p><a href="/">我的创作</a><a href="/studio/api">AI 内容索引 ↗</a></aside><section><small>YOUR CREATIVE WORKSPACE</small><h1>今天,想表达什么?</h1><p>一个页面,装下一个想法。让内容慢慢长成作品。</p><div className="grid">{ideas.map(i=><Link className="card" href={'/ideas/'+i.slug} key={i.slug}><div className={i.template?"cover cover-template":"cover"}>{i.template?<Ref17Cover template={i.template} slide={i.slides[0]}/>:i.title}</div><h2>{i.title}</h2><p>{i.summary}</p><span>打开创作 →</span></Link>)}</div><p className="hint">新增作品:在 src/content/ideas.ts 添加内容,页面与导出会同步生成。</p></section></main>}
+25
View File
@@ -1,2 +1,27 @@
@import "tailwindcss";
*{box-sizing:border-box}body{margin:0;color:#242424;background:#faf9f7;font-family:Arial,"PingFang SC",sans-serif}a{color:inherit;text-decoration:none}button,a{cursor:pointer}button{border:0;padding:12px 22px;border-radius:24px}.workspace{display:grid;grid-template-columns:240px 1fr;min-height:100vh}aside{padding:38px 26px;border-right:1px solid #eee;background:white}aside a{display:block;margin-top:30px}.logo{color:#ff2442}section{padding:70px 6vw}h1{font-size:40px;letter-spacing:-1.5px}section>p,.hint{color:#888}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,340px));gap:24px;margin-top:45px}.card{background:white;padding:14px;border-radius:22px;border:1px solid #eee}.cover{background:#ffe5df;aspect-ratio:4/5;border-radius:14px;padding:36px;display:flex;align-items:center;font-size:35px;font-weight:bold}.card h2{font-size:18px}.card p{line-height:1.8;color:#888}.card span{color:#ff2442}.hint{margin-top:40px;font-size:13px}.editor{max-width:1200px;margin:auto;padding:30px}header{display:flex;align-items:center;gap:24px}header a:first-child{margin-right:auto}.slides{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:40px}.slide{aspect-ratio:3/4;padding:35px;border-radius:20px;display:flex;flex-direction:column}.slide h2{font-size:38px;margin-top:auto}.slide p{line-height:1.9}.slide footer{margin-top:auto;font-size:12px}@media(max-width:760px){.workspace{grid-template-columns:1fr}aside{border-bottom:1px solid #eee}aside a{display:inline-block;margin:20px 20px 0 0}section{padding:30px}.slides{grid-template-columns:1fr}header{flex-wrap:wrap}h1{font-size:30px}}@media print{header{display:none}.slides{display:block}.slide{width:180mm;height:240mm;break-after:page;print-color-adjust:exact;border-radius:0}.editor>h1,.editor>p{display:none}}
/* The reference art stays inside its native 113:174 canvas. */
.cover-template{padding:0;border-radius:0;display:block;aspect-ratio:113/174;background:#f4f2eb}
.template-trial{max-width:1480px;margin:auto;padding:28px 40px 70px;color:#28362f}
.trial-nav{padding-bottom:24px;border-bottom:1px solid #dedfd8;font-size:13px;letter-spacing:.04em}
.trial-nav span{color:#7c847d}.trial-nav a:last-child{margin-left:auto}
.trial-intro{padding:48px 0 34px;display:flex;gap:32px;justify-content:space-between;align-items:end}
.trial-intro small{font-size:11px;letter-spacing:.12em;color:#758276}.trial-intro h1{font-size:44px;margin:16px 0;letter-spacing:-2px;line-height:1.2}.trial-intro p{max-width:650px;line-height:1.8;font-size:14px;color:#727b72}
.trial-source{display:grid;gap:10px;font-size:12px;min-width:140px;color:#758276}.trial-source b{color:#28362f}
.trial-workspace{display:grid;grid-template-columns:260px minmax(0,1fr);gap:32px;align-items:start}
.trial-controls{padding:22px;border:1px solid #dedfd8;background:#fff;border-radius:8px;position:sticky;top:20px}
.trial-controls h2{font-size:18px;margin:0 0 20px}.trial-controls label{display:block;font-size:12px;margin:14px 0;color:#4d594f}.trial-controls small{font-size:10px;color:#879087;margin-left:4px}
.trial-controls input:not([type=checkbox]),.trial-controls select,.trial-controls textarea{display:block;width:100%;border:1px solid #d8dcd5;border-radius:4px;padding:9px;margin-top:7px;background:#fafbf8;color:#28362f;font:inherit;line-height:1.5}
.trial-controls fieldset{border:0;border-top:1px solid #e5e8df;padding:12px 0 0;margin:20px 0}.trial-controls legend{font-size:11px;padding-right:8px;color:#879087}.trial-controls fieldset label{margin:8px 0}
.trial-colors{display:flex;gap:16px}.trial-colors label{flex:1}.trial-controls input[type=color]{height:36px;padding:4px}.trial-controls .trial-switch{display:flex;align-items:center;gap:8px;padding:12px 0}.trial-switch input{accent-color:#44614b}
.trial-actions{display:grid;gap:8px}.trial-actions button{padding:11px 8px;border:1px solid #dce1d7;border-radius:4px;font-size:12px;background:#fff;color:#455443}.trial-actions .trial-primary{background:#344f3c;color:white;border-color:#344f3c}.trial-actions button:hover{filter:brightness(.94)}
.trial-help{font-size:11px;line-height:1.7;color:#8a9288;margin-top:16px}.trial-notice{font-size:11px;color:#346844;min-height:16px}
.trial-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:28px}.proof-card{margin:0;min-width:0}.proof-select{display:block;width:100%;padding:0;border-radius:0;background:#f4f2eb;box-shadow:0 3px 14px #28362f14;outline:1px solid #dddcd3;outline-offset:0;transition:outline-color .15s}.proof-active .proof-select{outline:2px solid #617d65;outline-offset:5px}.proof-select:focus-visible{outline:3px solid #346844;outline-offset:5px}.proof-card figcaption{display:flex;gap:10px;align-items:center;padding:16px 0 0;font-size:11px;flex-wrap:wrap}.proof-card figcaption b{font-weight:500}.proof-card figcaption span{color:#92988e}.proof-card figcaption a{margin-left:auto;text-decoration:underline;text-underline-offset:3px;color:#61735e}
.trial-reference{margin-top:36px;padding:20px 0;border-top:1px solid #dedfd8;font-size:12px}.trial-reference summary{cursor:pointer}.trial-reference-body{display:grid;grid-template-columns:150px minmax(0,1fr);gap:24px;margin-top:22px;line-height:1.8}.trial-reference img{width:150px;height:auto;align-self:start}.trial-reference table{width:100%;border-collapse:collapse;font-size:11px;margin:16px 0}.trial-reference th,.trial-reference td{text-align:left;border-bottom:1px solid #dedfd8;padding:6px}.trial-reference a{text-decoration:underline}
@media(min-width:1300px){.trial-grid{gap:40px}.trial-workspace{gap:48px}}
@media(max-width:900px){.template-trial{padding:22px}.trial-workspace{grid-template-columns:220px minmax(0,1fr);gap:24px}.trial-grid{gap:20px}.trial-controls{padding:16px}.trial-source{display:none}.trial-intro h1{font-size:36px}.trial-nav span{display:none}}
@media(max-width:650px){.trial-workspace{grid-template-columns:1fr}.trial-controls{position:static}.trial-controls fieldset{display:grid;grid-template-columns:1fr 1fr;gap:12px}.trial-actions{grid-template-columns:1fr 1fr}.trial-reference-body{grid-template-columns:1fr}.trial-grid{gap:24px}.trial-intro{padding:32px 0}.trial-intro h1{font-size:30px}}
@media(max-width:440px){.trial-grid{grid-template-columns:1fr}.trial-nav{gap:10px}.template-trial{padding:18px}}
@page ref17Proof{size:113mm 174mm;margin:0}
@media print{.template-trial{padding:0;max-width:none}.trial-nav,.trial-intro,.trial-controls,.trial-reference,.proof-card figcaption{display:none}.trial-workspace,.trial-grid{display:block}.proof-card{page:ref17Proof;break-after:page;width:113mm;height:174mm}.proof-card:last-child{break-after:auto}.proof-select,.proof-active .proof-select{outline:none;box-shadow:none;width:113mm;height:174mm}.proof-select svg{width:113mm!important;height:174mm!important;print-color-adjust:exact}.proof-select svg g[aria-hidden=true]{display:none}}