feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)

Real IMAP connection via imapflow library:
- Connects to IMAP server (IMAPFlow, TLS on port 993)
- Fetches recent messages (configurable limit, default 20)
- Extracts: from, fromEmail, subject, date, snippet (200 chars)
- Thread-to-contact matching (PRD §6.1):
  1. Match sender email against persons table
  2. If no person match, extract domain and match against company.website
  3. Returns matchedCompany + matchedPerson for each email
- Audit-logged (action: crm.mailsync:imap)
- Zod validated (host, port, user, password, folder, limit)
- 502 on IMAP connection failure with detail
- 400 on validation failure

Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
Z User committed 2026-10-27 22:50:50 +00:00
1 parent 7acc0c5451
commit e45b68bb8b
1089 files changed
+260614

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
---
name: social-card-swiss
description: Swiss 网格 / 系统派社交卡 — 精确 / 系统化 / 技术感 / 方法论调性。卡角色含 system 封面 / numbered method / 对比 before-after / KPI tower / 横条 mini-chart / 截图解释 / 证据墙 / 收尾 checklist。用于 framework / checklist / 工具说明 / AI workflow / 产品更新 / KPI 总结。触发:"做个方法卡 / 框架卡 / 工具拆解卡 / AI 工具卡 / 系统派结构卡"。NOT 情绪 / 文学 / 图主导卡,NOT 软生活 pastel 风。
visibility: public
mode: template
carrier: fixed-image
scenario: social-content
pattern_source: pattern.html
source_priority: skill-first
triggers:
- "做个方法卡"
- "框架卡"
- "工具拆解卡"
- "AI 工具卡"
- "系统派卡"
- "结构化知识卡"
- "Swiss 网格卡"
related_patterns: social-card-data-kpi, social-card-screenshot-explainer
---
# Social Card Swiss Template
Use this template for structured social cards that should feel precise, systematic, technical, or method-driven.
## Use When
- The content is a framework, checklist, comparison, tool explanation, AI workflow, product update, KPI summary, or screenshot explainer.
- The design should use grid, numbering, labels, metrics, or modular blocks.
- The user wants a clean modern card that feels designed but not decorative.
## Avoid When
- The content is emotional, literary, image-led, or slow editorial.
- The user wants a soft lifestyle/pastel look.
- A plain poster or deck slide is a better fit than a social card.
## Required Reading
Read this `SKILL.md` first. Do **not** read `pattern.html` by default.
Read `pattern.html` only if the selected card needs layout relationships that are hard to infer from this file. Extract only grid rhythm, component relationships, responsive/card behavior, and section hierarchy.
## Design Strategy
Choose a card role before choosing visual detail:
- system cover
- numbered method
- comparison / before-after
- KPI tower
- horizontal bar / mini chart
- screenshot explainer
- evidence wall
- closing checklist
## Layout Bias
Prefer:
- strict grid and alignment
- numbering systems and small metadata labels
- clear typographic hierarchy
- compact but readable card sections
- data blocks, comparison rows, and interface frames
Avoid:
- random stickers, emoji icons, and decorative blobs
- unclear focal hierarchy
- screenshots too small to read
- generic SaaS feature-card layouts
## Platform / Size
Use `canvas-and-device.md` for platform dimensions and export ratios. This template supplies structure, not fixed size.
@@ -0,0 +1,160 @@
/* Reusable Guizang-style electronic magazine background.
Copy or inline into generated HTML. It renders a deterministic WebGL ink-flow
canvas and falls back to 2D if WebGL is unavailable. */
(function () {
function rgb(input, fallback) {
return Array.isArray(input) && input.length === 3 ? input.map((v) => v / 255) : fallback;
}
function fallback2d(canvas, opts) {
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = Math.min(2, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
const ink = opts.inkCss || "rgba(26,46,31,.22)";
const accent = opts.accentCss || "rgba(46,107,79,.18)";
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 5; i++) {
const x = w * (0.14 + ((i * 0.19 + 0.17) % 0.72));
const y = h * (0.10 + ((i * 0.29 + 0.23) % 0.78));
const r = Math.max(w, h) * (0.22 + i * 0.045);
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, i % 2 ? accent : ink);
g.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
}
ctx.globalAlpha = 0.09;
ctx.strokeStyle = opts.inkCss || "rgba(26,46,31,.22)";
for (let i = 0; i < 18; i++) {
ctx.beginPath();
ctx.ellipse(w * (0.5 + Math.sin(i) * 0.34), h * (0.5 + Math.cos(i * 1.7) * 0.28), 120 + i * 18, 42 + i * 9, i * 0.37, 0, Math.PI * 2);
ctx.stroke();
}
ctx.globalAlpha = 1;
}
function mount(canvas, options) {
const opts = options || {};
const gl = canvas.getContext("webgl", { alpha: true, antialias: true, preserveDrawingBuffer: true });
if (!gl) {
fallback2d(canvas, opts);
return { mode: "2d" };
}
const vert = `
attribute vec2 aPos;
void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }
`;
const frag = `
precision mediump float;
uniform vec2 uRes;
uniform float uTime;
uniform vec3 uInk;
uniform vec3 uPaper;
uniform vec3 uAccent;
uniform float uStrength;
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
float noise(vec2 p){
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i + vec2(0.0,0.0)), hash(i + vec2(1.0,0.0)), u.x),
mix(hash(i + vec2(0.0,1.0)), hash(i + vec2(1.0,1.0)), u.x),
u.y
);
}
float fbm(vec2 p){
float v = 0.0;
float a = 0.5;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for(int i=0; i<5; i++){
v += a * noise(p);
p = m * p * 1.18;
a *= 0.52;
}
return v;
}
void main(){
vec2 uv = gl_FragCoord.xy / uRes.xy;
vec2 p = (gl_FragCoord.xy - 0.5 * uRes.xy) / min(uRes.x, uRes.y);
float t = uTime * 0.045;
float n = fbm(p * 2.2 + vec2(t, -t * 0.7));
float n2 = fbm(p * 5.4 - vec2(t * 1.7, t));
float contour = smoothstep(0.015, 0.0, abs(fract((n + n2 * 0.35) * 7.0) - 0.5));
float wash = smoothstep(0.22, 0.88, n);
float vignette = 1.0 - smoothstep(0.28, 0.92, length(p));
vec3 col = mix(uPaper, uInk, wash * 0.42 + contour * 0.18);
col = mix(col, uAccent, (0.18 + 0.25 * n2) * contour);
float alpha = uStrength * (0.18 + wash * 0.42 + contour * 0.30) * (0.55 + 0.45 * vignette);
gl_FragColor = vec4(col, alpha);
}
`;
function shader(type, source) {
const sh = gl.createShader(type);
gl.shaderSource(sh, source);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh));
return sh;
}
const program = gl.createProgram();
gl.attachShader(program, shader(gl.VERTEX_SHADER, vert));
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, frag));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
gl.useProgram(program);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(program, "aPos");
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
const uRes = gl.getUniformLocation(program, "uRes");
const uTime = gl.getUniformLocation(program, "uTime");
const uInk = gl.getUniformLocation(program, "uInk");
const uPaper = gl.getUniformLocation(program, "uPaper");
const uAccent = gl.getUniformLocation(program, "uAccent");
const uStrength = gl.getUniformLocation(program, "uStrength");
const ink = rgb(opts.ink, [0.10, 0.18, 0.12]);
const paper = rgb(opts.paper, [0.96, 0.94, 0.90]);
const accent = rgb(opts.accent, [0.18, 0.42, 0.31]);
function resize() {
const dpr = Math.min(2, window.devicePixelRatio || 1);
const rect = canvas.getBoundingClientRect();
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
gl.viewport(0, 0, canvas.width, canvas.height);
}
function draw(time) {
resize();
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, opts.frozenTime == null ? time * 0.001 : opts.frozenTime);
gl.uniform3f(uInk, ink[0], ink[1], ink[2]);
gl.uniform3f(uPaper, paper[0], paper[1], paper[2]);
gl.uniform3f(uAccent, accent[0], accent[1], accent[2]);
gl.uniform1f(uStrength, opts.strength == null ? 0.34 : opts.strength);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
draw(0);
if (opts.frozenTime == null) requestAnimationFrame(draw);
return { mode: "webgl", redraw: () => draw(0) };
}
window.MagazineBg = { mount };
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

@@ -0,0 +1,943 @@
<!doctype html>
<html lang="zh-CN" data-accent="ikb">
<head>
<meta charset="utf-8">
<title>[必填] 替换为社交卡组标题 · Social Card Set</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&family=Noto+Sans+SC:wght@200;300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<style>
/* =========================================================
Swiss International — Social Card Seed Template
---------------------------------------------------------
Three boards built in one HTML:
.poster.xhs 1080×1440 (3:4 小红书)
.poster.square 1080×1080 (1:1 微信方封面)
.poster.wide 2100× 900 (21:9 微信主封面)
.pair-preview 21:9 + 1:1 配对预览
Accent switching via <html data-accent="…">:
ikb (Klein Blue) | lemon-yellow | lemon-green | safety-orange
Fill posters where marked <!-- POSTERS_HERE -->.
Each poster carries one Layout Recipe (S01-S12) from
references/layout-recipes.md.
========================================================= */
/* ---------- 1. Accent palettes (4 mutually exclusive) ---------- */
:root,
[data-accent="ikb"] {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #002FA7;
--accent-on: #ffffff;
}
[data-accent="lemon-yellow"] {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #FFD500;
--accent-on: #0a0a0a;
}
[data-accent="lemon-green"] {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #C5E803;
--accent-on: #0a0a0a;
}
[data-accent="safety-orange"] {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #FF6B35;
--accent-on: #ffffff;
}
/* ---------- 2. Typography stack ---------- */
:root {
--sans: "Inter", "Helvetica Neue", Helvetica, "Noto Sans SC",
-apple-system, "PingFang SC", "Microsoft YaHei UI", sans-serif;
--sans-zh: "Noto Sans SC", -apple-system, "PingFang SC",
"Microsoft YaHei UI", "Inter", sans-serif;
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Consolas, monospace;
}
/* ---------- 3. Spacing tokens (8/12/16/24/32/40/48/64/80/96/160) ---------- */
:root {
--sp-3: 8px;
--sp-4: 12px;
--sp-5: 16px;
--sp-6: 24px;
--sp-7: 32px;
--sp-8: 40px;
--sp-9: 48px;
--sp-10: 64px;
--sp-11: 80px;
--sp-12: 96px;
--sp-13:160px;
}
/* ---------- 4. Reset & sheet ---------- */
*,*::before,*::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: #1a1a1a;
font-family: var(--sans);
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 64px 32px;
}
.sheet {
display: flex;
flex-direction: column;
align-items: center;
gap: 48px;
}
/* ---------- 5. Poster boards (fixed export sizes) ---------- */
.poster {
position: relative;
background: var(--paper);
color: var(--ink);
overflow: hidden;
isolation: isolate;
}
.poster.xhs { width: 1080px; height: 1440px; }
.poster.square { width: 1080px; height: 1080px; }
.poster.wide { width: 2100px; height: 900px; }
.poster figure { margin: 0; }
.poster.xhs .content { padding: var(--sp-12) var(--sp-11); }
.poster.square .content { padding: var(--sp-11) var(--sp-11); }
.poster.wide .content { padding: var(--sp-11) var(--sp-13); }
.content { position: relative; width: 100%; height: 100%; z-index: 2; }
/* Optional decorative layers — opt in per poster */
.dot-mat,
.ring-mat,
.cross-mat {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
}
.dot-mat {
opacity: .08;
background-image: radial-gradient(var(--ink) 1.5px, transparent 1.5px);
background-size: 24px 24px;
}
.ring-mat {
opacity: .08;
background-image: radial-gradient(circle at 50% 50%, transparent 4px, var(--ink) 4.5px, var(--ink) 5px, transparent 5.5px);
background-size: 32px 32px;
}
.cross-mat {
opacity: .06;
background-image:
linear-gradient(var(--ink), var(--ink)),
linear-gradient(90deg, var(--ink), var(--ink));
background-size: 1px 14px, 14px 1px;
background-position: center, center;
background-repeat: repeat;
}
/* ---------- 6. Typography roles (Swiss extreme contrast) ----------
Hard rule: "the larger, the lighter"
≥ 200px → weight 200 (ExtraLight)
Smaller text gets heavier weight (≥ 500). */
.h-hero {
font-family: var(--sans);
font-weight: 200;
font-size: 240px;
line-height: 1.02;
letter-spacing: -.02em;
margin: 0;
color: var(--ink);
}
.h-statement {
font-family: var(--sans);
font-weight: 200;
font-size: 180px;
line-height: 1.05;
letter-spacing: -.015em;
margin: 0;
color: var(--ink);
}
.h-xl {
font-family: var(--sans);
font-weight: 300;
font-size: 120px;
line-height: 1.08;
letter-spacing: -.01em;
margin: 0;
color: var(--ink);
}
.h-md {
font-family: var(--sans);
font-weight: 400;
font-size: 56px;
line-height: 1.18;
margin: 0;
color: var(--ink);
}
.lead {
font-family: var(--sans-zh);
font-weight: 400;
font-size: 30px;
line-height: 1.55;
color: var(--ink);
margin: 0;
}
.body {
font-family: var(--sans-zh);
font-weight: 400;
font-size: 26px;
line-height: 1.6;
color: var(--ink);
margin: 0;
}
.t-cat {
font-family: var(--sans);
font-weight: 600;
font-size: 22px;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--accent);
margin: 0;
}
.t-meta {
font-family: var(--mono);
font-weight: 500;
font-size: 20px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--grey-3);
margin: 0;
}
.num-mega {
font-family: var(--sans);
font-weight: 200;
font-size: 200px;
line-height: .92;
letter-spacing: -.03em;
color: var(--ink);
margin: 0;
}
.num-xl {
font-family: var(--sans);
font-weight: 200;
font-size: 144px;
line-height: .96;
letter-spacing: -.02em;
margin: 0;
}
.mono { font-family: var(--mono); }
/* XHS (3:4) boards: 1080×1440 has the tightest vertical budget,
so display sizes step down vs the .wide default. These caps
were validated by the smoke-ai-tools deck — 2-line h-xl @96px
leaves room for matrix + hero-stat-bottom inside 1440px. */
.poster.xhs .h-hero { font-size: 168px; }
.poster.xhs .h-statement { font-size: 124px; }
.poster.xhs .h-xl { font-size: 96px; }
.poster.xhs .num-mega { font-size: 168px; }
.poster.xhs .num-xl { font-size: 120px; }
/* Square boards tighten scales */
.poster.square .h-hero { font-size: 180px; }
.poster.square .h-statement { font-size: 140px; }
.poster.square .h-xl { font-size: 88px; }
.poster.square .num-mega { font-size: 168px; }
.poster.square .num-xl { font-size: 112px; }
/* Wide boards rely on horizontal real estate */
.poster.wide .h-hero { font-size: 200px; }
.poster.wide .h-statement { font-size: 156px; }
.poster.wide .h-xl { font-size: 104px; }
/* ---------- 7. Layout primitives ---------- */
.stack { display: flex; flex-direction: column; }
.row { display: flex; flex-direction: row; }
.gap-3 { gap: var(--sp-3); }
.gap-4 { gap: var(--sp-4); }
.gap-5 { gap: var(--sp-5); }
.gap-6 { gap: var(--sp-6); }
.gap-7 { gap: var(--sp-7); }
.gap-8 { gap: var(--sp-8); }
.gap-9 { gap: var(--sp-9); }
.gap-10 { gap: var(--sp-10); }
.grow { flex: 1; }
.grid-12 { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--sp-7); }
.grid-2-9 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-9); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-7); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-6); }
.span-2 { grid-column: span 2; }
.span-3 { grid-column: span 3; }
.span-4 { grid-column: span 4; }
.span-6 { grid-column: span 6; }
.span-8 { grid-column: span 8; }
.span-9 { grid-column: span 9; }
.span-12 { grid-column: span 12; }
.center { text-align: center; }
.right { text-align: right; }
.uppercase{ text-transform: uppercase; }
.hr-hairline {
height: 1px;
background: var(--grey-2);
border: 0;
margin: 0;
}
.hr-accent {
height: 3px;
background: var(--accent);
border: 0;
width: 96px;
margin: 0;
}
/* ---------- 8. Card fills (mutually exclusive — pick ONE) ----------
Hard rule: never combine card-ink/card-accent/card-fill/card-outlined
on the same node. Multi-card grids must use the same class. */
.card-ink {
background: var(--ink);
color: var(--paper);
padding: var(--sp-8);
}
.card-ink .t-cat,
.card-ink .t-meta,
.card-ink .lead,
.card-ink .body,
.card-ink .h-md { color: var(--paper); }
.card-accent {
background: var(--accent);
color: var(--accent-on);
padding: var(--sp-8);
}
.card-accent .t-cat,
.card-accent .t-meta,
.card-accent .lead,
.card-accent .body,
.card-accent .h-md { color: var(--accent-on); }
.card-fill {
background: var(--grey-1);
color: var(--ink);
padding: var(--sp-8);
}
.card-outlined {
background: transparent;
color: var(--ink);
border: 1px solid var(--grey-2);
padding: var(--sp-8);
}
/* ---------- 9. Frame containers (image holders) ---------- */
.frame-img {
position: relative;
overflow: hidden;
background: var(--grey-1);
display: block;
margin: 0;
}
.frame-img img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 35%;
display: block;
}
.frame-img.fit-contain img { object-fit: contain; background: var(--paper); }
.frame-img.r-3x4 { aspect-ratio: 3 / 4; }
.frame-img.r-1x1 { aspect-ratio: 1 / 1; }
.frame-img.r-4x3 { aspect-ratio: 4 / 3; }
.frame-img.r-16x9 { aspect-ratio: 16 / 9; }
.frame-img.r-16x10 { aspect-ratio: 16 / 10; }
.frame-img.r-21x9 { aspect-ratio: 21 / 9; }
.swiss-img-caption {
font-family: var(--mono);
font-weight: 500;
font-size: 18px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--grey-3);
margin-top: var(--sp-5);
}
/* ---------- 9b. Screenshot treatment (.frame-shot) ---------- */
/* Swiss is stricter than Editorial: corners default square, shadows default
none. Override only when the shot must literally float off the page. */
.frame-shot {
position: relative;
overflow: hidden;
display: block;
margin: 0;
background: var(--grey-1);
border-radius: 0;
}
.frame-shot.corners-sq { border-radius: 0; }
.frame-shot.corners-sm { border-radius: 4px; }
.frame-shot.corners-md { border-radius: 10px; }
/* Swiss shadows are flat — only the subtle outline scrim is welcome */
.frame-shot.shadow-soft { box-shadow: 0 12px 28px -16px rgba(0,0,0,.18); }
.frame-shot.shadow-ed { box-shadow: 0 20px 40px -20px rgba(0,0,0,.22), 0 0 0 1px var(--grey-2); }
/* background tones — pure / dot / grid only. No accent colour fills. */
.frame-shot.bg-paper { background: var(--paper); }
.frame-shot.bg-grey-1 { background: var(--grey-1); }
.frame-shot.bg-ink { background: var(--ink); }
.frame-shot.bg-grid {
background-color: var(--paper);
background-image:
linear-gradient(to right, var(--grey-2) 1px, transparent 1px),
linear-gradient(to bottom, var(--grey-2) 1px, transparent 1px);
background-size: 48px 48px;
}
.frame-shot.bg-dot {
background-color: var(--paper);
background-image: radial-gradient(var(--grey-2) 1px, transparent 1px);
background-size: 18px 18px;
}
/* asset backgrounds — ported from PPT skill, real texture for screenshot wells.
Use ONLY when current accent matches the asset's accent (don't mix oranges and IKB). */
.frame-shot.bg-asset-ikb-dot { background: #0c1a3a url("assets/screenshot-backgrounds/style-b/ikb-dot-gradient.webp") center/cover no-repeat; }
.frame-shot.bg-asset-lemon-green-dot { background: #d6e08a url("assets/screenshot-backgrounds/style-b/lemon-green-dot-shadow.webp") center/cover no-repeat; }
.frame-shot.bg-asset-lemon-grid { background: #e6e9b8 url("assets/screenshot-backgrounds/style-b/lemon-grid.webp") center/cover no-repeat; }
.frame-shot.bg-asset-safety-orange { background: #ff5b1f url("assets/screenshot-backgrounds/style-b/safety-orange-halftone.webp") center/cover no-repeat; }
.frame-shot.inset-none > img,
.frame-shot.inset-none > .shot-body { padding: 0; }
.frame-shot.inset-sub { padding: 20px; }
.frame-shot.inset-bal { padding: 48px; }
.frame-shot.r-3x4 { aspect-ratio: 3 / 4; }
.frame-shot.r-1x1 { aspect-ratio: 1 / 1; }
.frame-shot.r-4x3 { aspect-ratio: 4 / 3; }
.frame-shot.r-16x9 { aspect-ratio: 16 / 9; }
.frame-shot.r-16x10 { aspect-ratio: 16 / 10; }
.frame-shot.r-21x9 { aspect-ratio: 21 / 9; }
.frame-shot.r-3x2 { aspect-ratio: 3 / 2; }
.frame-shot > img {
width: 100%;
height: 100%;
object-fit: contain;
object-position: center;
display: block;
border-radius: inherit;
}
.frame-shot.fit-cover > img { object-fit: cover; }
/* device chrome — CSS-only, no SVG */
.device-browser {
position: relative;
background: var(--paper);
border: 1px solid var(--grey-2);
border-radius: 6px;
overflow: hidden;
}
.device-browser::before {
content: '';
display: block;
height: 32px;
background: var(--grey-1);
border-bottom: 1px solid var(--grey-2);
background-image:
radial-gradient(circle at 16px 16px, var(--grey-3) 4px, transparent 4.5px),
radial-gradient(circle at 34px 16px, var(--grey-3) 4px, transparent 4.5px),
radial-gradient(circle at 52px 16px, var(--grey-3) 4px, transparent 4.5px);
}
.device-browser > .frame-shot,
.device-browser > img {
border-radius: 0;
display: block;
width: 100%;
}
.device-phone {
position: relative;
background: var(--ink);
border-radius: 28px;
padding: 12px;
box-shadow: 0 16px 36px -18px rgba(0,0,0,.32);
}
.device-phone > .frame-shot,
.device-phone > img {
border-radius: 18px;
display: block;
width: 100%;
}
/* ---------- 9c. Map block (.map-block) ---------- */
/* Static map for Swiss decks. Pins are pure HTML, no SVG text.
Use accent sparingly: at most one accent pin per board. */
.map-block {
position: relative;
background: var(--grey-1);
overflow: hidden;
aspect-ratio: 4 / 3;
width: 100%;
}
.map-block.r-16x10 { aspect-ratio: 16 / 10; }
.map-block.r-16x9 { aspect-ratio: 16 / 9; }
.map-block.r-1x1 { aspect-ratio: 1 / 1; }
.map-block.r-3x4 { aspect-ratio: 3 / 4; }
.map-block > img,
.map-block > svg {
position: absolute; inset: 0;
width: 100%; height: 100%;
display: block;
}
.map-block > img { object-fit: cover; }
/* desaturated tile filter to keep Swiss strict tone */
.map-block.tone-paper > img { filter: saturate(0) contrast(1.05) brightness(1.02); }
.map-block.tone-ink > img { filter: saturate(0) brightness(.58); }
/* schematic SVG defaults */
.map-block > svg .map-coast { fill: none; stroke: var(--grey-2); stroke-width: .4; }
.map-block > svg .map-road { fill: none; stroke: var(--grey-3); stroke-width: .28; stroke-dasharray: 1.2 .8; opacity: .7; }
.map-block > svg .map-water { fill: var(--grey-1); }
.map-block > svg .map-grid { stroke: var(--grey-2); stroke-width: .15; opacity: .5; }
.map-pin {
position: absolute;
width: 0; height: 0;
transform: translate(-50%, -50%);
}
.map-pin .dot {
position: absolute;
left: -6px; top: -6px;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--ink);
border: 2px solid var(--paper);
box-shadow: 0 1px 3px rgba(0,0,0,.22);
}
.map-pin.accent .dot { background: var(--accent); }
.map-pin .line {
position: absolute;
left: 7px; top: 0;
width: 28px; height: 1px;
background: var(--ink);
opacity: .55;
}
.map-pin.left .line { left: auto; right: 7px; }
.map-pin .card {
position: absolute;
left: 38px; top: -20px;
min-width: 80px;
padding: 8px 10px;
background: rgba(250,250,248,.94);
box-shadow: 0 0 0 1px var(--grey-2);
border-radius: 2px;
white-space: nowrap;
}
.map-pin.left .card { left: auto; right: 38px; }
.map-pin .card .name {
font-family: var(--sans-zh), var(--sans);
font-weight: 500;
font-size: 15px;
line-height: 1.1;
color: var(--ink);
}
.map-pin .card .meta {
display: block;
margin-top: 4px;
font-family: var(--mono);
font-size: 10px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--grey-3);
}
.map-pin.accent .card .name { color: var(--accent); }
.map-legend {
position: absolute;
left: 14px; bottom: 14px;
z-index: 3;
background: rgba(250,250,248,.94);
padding: 8px 12px;
box-shadow: 0 0 0 1px var(--grey-2);
font-family: var(--mono);
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--grey-3);
}
.map-legend.tr { left: auto; right: 14px; top: 14px; bottom: auto; }
/* ---------- 10. Swiss-specific components ---------- */
/* S08 Image Hero (port of S22) */
.image-hero {
display: grid;
grid-template-rows: auto 1fr;
gap: var(--sp-7);
height: 100%;
}
.image-hero .hero-img-wrap {
position: relative;
overflow: hidden;
background: var(--grey-1);
}
.image-hero .hero-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 35%;
display: block;
}
.image-hero .hero-overlay-block {
position: absolute;
left: 0;
top: 0;
background: var(--paper);
padding: var(--sp-7) var(--sp-8);
max-width: 60%;
}
.image-hero .hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--sp-8);
align-items: end;
border-top: 1px solid var(--grey-2);
padding-top: var(--sp-7);
}
.image-hero .hero-stats .stat-block .num {
font-family: var(--sans);
font-weight: 200;
font-size: 96px;
line-height: 1;
letter-spacing: -.02em;
color: var(--ink);
margin: 0 0 var(--sp-4);
}
.image-hero .hero-stats .stat-block .lbl {
font-family: var(--mono);
font-size: 18px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--grey-3);
margin: 0;
}
/* S09 KPI Tower (port of S06) */
.kpi-tower-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--sp-7);
align-items: end;
height: 100%;
}
.kpi-tower-row .tower-col {
display: flex;
flex-direction: column;
gap: var(--sp-5);
align-items: flex-start;
}
.kpi-tower-row .tower-col .num {
font-family: var(--sans);
font-weight: 200;
font-size: 112px;
line-height: 1;
letter-spacing: -.02em;
margin: 0;
}
.kpi-tower-row .tower-col .lbl {
font-family: var(--mono);
font-size: 20px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--grey-3);
margin: 0;
}
.kpi-tower-row .tower-col .bar-tower {
width: 100%;
height: var(--h, 240px);
background: var(--accent);
transform-origin: bottom;
}
.kpi-tower-row .tower-col.muted .bar-tower {
background: var(--grey-2);
}
.poster.xhs .kpi-tower-row { grid-template-columns: 1fr 1fr; gap: var(--sp-9); }
.poster.square .kpi-tower-row { grid-template-columns: 1fr 1fr; gap: var(--sp-9); }
/* S10 H-Bar Chart (port of S07) */
.h-bar-chart {
display: grid;
gap: var(--sp-6);
}
.h-bar-chart .bar-row {
display: grid;
grid-template-columns: 220px 1fr 120px;
gap: var(--sp-6);
align-items: center;
}
.h-bar-chart .bar-row .row-lbl {
font-family: var(--sans-zh);
font-weight: 500;
font-size: 26px;
color: var(--ink);
}
.h-bar-chart .bar-row .row-track {
position: relative;
height: 28px;
background: var(--grey-1);
}
.h-bar-chart .bar-row .row-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: var(--w, 50%);
background: var(--accent);
}
.h-bar-chart .bar-row .row-val {
font-family: var(--mono);
font-weight: 500;
font-size: 26px;
letter-spacing: .04em;
text-align: right;
color: var(--ink);
}
/* On portrait the labels move above the track */
.poster.xhs .h-bar-chart .bar-row {
grid-template-columns: 1fr;
gap: var(--sp-4);
}
.poster.xhs .h-bar-chart .bar-row .row-val {
text-align: left;
}
/* S11 Stacked Ledger (port of S20) */
.stacked-ledger {
display: flex;
flex-direction: column;
}
.stacked-ledger .ledger-row {
display: grid;
grid-template-columns: 280px 1fr auto;
gap: var(--sp-7);
align-items: baseline;
padding: var(--sp-7) 0;
border-bottom: 1px solid var(--grey-2);
}
.stacked-ledger .ledger-row:first-child { padding-top: 0; }
.stacked-ledger .ledger-row:last-child { border-bottom: 0; }
.stacked-ledger .ledger-num {
font-family: var(--sans);
font-weight: 200;
font-size: 112px;
line-height: 1;
letter-spacing: -.02em;
margin: 0;
}
.stacked-ledger .ledger-lbl {
font-family: var(--sans-zh);
font-weight: 500;
font-size: 30px;
line-height: 1.3;
color: var(--ink);
}
.stacked-ledger .ledger-lbl .sub {
display: block;
font-weight: 400;
font-size: 22px;
color: var(--grey-3);
margin-top: var(--sp-3);
}
.stacked-ledger .ledger-icn {
width: 56px;
height: 56px;
color: var(--accent);
}
.poster.square .stacked-ledger .ledger-num { font-size: 88px; }
.poster.square .stacked-ledger .ledger-row { grid-template-columns: 220px 1fr auto; }
/* S12 Matrix Fill (port of S15) */
.matrix-fill {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 1fr;
gap: var(--sp-5);
}
.matrix-fill .matrix-cell {
background: var(--grey-1);
padding: var(--sp-6);
display: flex;
flex-direction: column;
gap: var(--sp-4);
min-height: 160px;
}
.matrix-fill .matrix-cell .cell-nb {
font-family: var(--mono);
font-size: 18px;
letter-spacing: .12em;
color: var(--grey-3);
}
.matrix-fill .matrix-cell .cell-title {
font-family: var(--sans-zh);
font-weight: 500;
font-size: 26px;
line-height: 1.3;
color: var(--ink);
}
.matrix-fill .matrix-cell.is-accent {
background: var(--accent);
color: var(--accent-on);
}
.matrix-fill .matrix-cell.is-accent .cell-nb,
.matrix-fill .matrix-cell.is-accent .cell-title { color: var(--accent-on); }
.hero-stat-bottom {
margin-top: var(--sp-9);
padding-top: var(--sp-7);
border-top: 1px solid var(--grey-2);
display: grid;
grid-template-columns: 1fr auto;
align-items: end;
gap: var(--sp-7);
}
/* xhs uses 2 columns; up to 8 cells = 4 rows. Switch to min-content
so rows don't stretch and crowd out hero-stat-bottom (validated
in smoke-ai-tools/xhs-04). When pairing matrix-fill with
hero-stat-bottom inside .poster.xhs, prefer 6 cells (3 rows). */
.poster.xhs .matrix-fill {
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: min-content;
gap: var(--sp-4);
}
.poster.xhs .matrix-fill .matrix-cell { min-height: 0; padding: var(--sp-5); }
.poster.xhs .matrix-fill .matrix-cell .cell-title { font-size: 24px; }
.poster.xhs .hero-stat-bottom {
margin-top: var(--sp-6);
padding-top: var(--sp-5);
align-items: center;
}
.poster.xhs .hero-stat-bottom .num-mega { font-size: 128px; line-height: 1; }
.poster.square .matrix-fill{ grid-template-columns: repeat(3, 1fr); grid-auto-rows: auto; }
.poster.square .matrix-fill .matrix-cell { min-height: 0; }
/* ---------- 11. Issue label + corner metadata ---------- */
.chrome-min {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--mono);
font-size: 18px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--grey-3);
padding-bottom: var(--sp-6);
border-bottom: 1px solid var(--grey-2);
margin-bottom: var(--sp-9);
}
/* ---------- 12. Pair preview frame ---------- */
.pair-preview {
width: 2400px;
min-height: 1180px;
background: var(--grey-1);
padding: var(--sp-9);
display: grid;
grid-template-columns: 2100px 1080px;
grid-template-rows: 900px 1080px;
gap: var(--sp-9);
align-content: center;
justify-content: center;
}
.pair-preview .preview-wide { grid-column: 1; grid-row: 1 / span 2; align-self: center; }
.pair-preview .preview-square { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
.pair-preview .preview-label {
font-family: var(--mono);
font-weight: 500;
font-size: 18px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--grey-3);
margin-bottom: var(--sp-5);
}
/* ---------- 13. Hard rules (visual guard rails) ----------
Swiss style refuses these — uncomment to find offenders.
.poster *[style*="border-radius"],
.poster *[style*="box-shadow"],
.poster *[style*="linear-gradient"] { outline: 4px solid red !important; }
*/
</style>
</head>
<body>
<!-- ACCENT_OVERRIDE
Change <html data-accent="…"> above to:
ikb | lemon-yellow | lemon-green | safety-orange
One deck — one accent. Never combine.
-->
<main class="sheet">
<!-- POSTERS_HERE -->
<!--
Replace the placeholder poster below with real content.
Each poster carries one Layout Recipe (S01-S12) from
references/layout-recipes.md.
Available board sizes:
<section class="poster xhs" id="xhs-01">…</section> 3:4
<section class="poster square" id="wechat-1x1">…</section> 1:1
<section class="poster wide" id="wechat-21x9">…</section> 21:9
For WeChat covers, also include a <section class="pair-preview">
that places the 21:9 + 1:1 side-by-side for review.
Hard rules every poster must honor:
- exactly one --accent in the entire deck
- no border-radius, no box-shadow, no gradients
- card-ink / card-accent / card-fill / card-outlined are
mutually exclusive — never combine on one node
- "the larger, the lighter": ≥ 200px must use weight 200
-->
<!-- ============================================================
Placeholder: S01 Accent Cover (3:4)
Replace with real cover content from layout-recipes.md.
============================================================ -->
<section class="poster xhs" id="xhs-01">
<div class="content stack gap-9">
<div class="chrome-min">
<span>Vol. 01 · Swiss</span>
<span>2026.05</span>
</div>
<div class="stack gap-7">
<p class="t-cat">封面 · Cover</p>
<h1 class="h-statement">替换<br>标题</h1>
</div>
<div class="grow"></div>
<hr class="hr-accent">
<p class="lead">用一句克制的解释,告诉读者这一组卡片在讲什么。</p>
<div class="row gap-6">
<p class="t-meta">Issue 01</p>
<p class="t-meta">/ [作者]</p>
</div>
</div>
</section>
</main>
<!-- Lucide icon initialization (used by stacked ledger / matrix accents) -->
<script>
if (window.lucide && typeof window.lucide.createIcons === 'function') {
window.lucide.createIcons();
}
</script>
</body>
</html>
@@ -0,0 +1,152 @@
# Image Overlay & Subject Safety
Two rules that govern any poster where text sits on top of an image — full-bleed background, large photo well with title bar, or generated/AI image with text overlay.
A composition can pass HTML linting and still be unreadable. Both rules below must clear before you ship.
## Rule 1 — Selection first, mask only if selection fails
The default editorial-magazine answer is **no mask**. Real magazine covers (Kinfolk, Cereal, Apartamento, Monocle) almost never lay a uniform black gradient over a hero photo. They rely on **photo selection + composition** so text lands in a quiet zone naturally. If you cannot find such a photo, the answer is usually to swap the photo, not to caulk over a bad one with a 70%-black scrim.
Apply the steps in order. Stop at the first one that passes.
### Step 1 — Photo selection (the main lever)
Before picking a mask, verify the photo qualifies for full-bleed treatment. Both tests must pass:
**Quiet-zone test**: at least one band of ≥30% canvas (full width × ≥30% height, or full height × ≥30% width) is **low-detail / low-contrast / uniform**. Examples: out-of-focus background, deep shade, fog, calm water, plain sky, blurred grass. This band is where the title will land.
**Light test**: the photo carries **atmospheric / restrained light** — overcast, dawn fog, golden hour, forest understory, film softness, dusk silhouette. Reject high-saturation noon shots, on-camera flash, "tourist trap" selfies, generic stock cheerfulness.
If either test fails, **the photo is wrong for M16**. Fall back to M01 (split-layout cover with photo in a frame), or shoot/source again. Do not "fix it with a mask."
### Step 2 — Compose without a mask first
Place the title inside the qualified quiet zone. Run the thumbnail contrast check (Step 4). If it reads clean, ship as-is. Most well-selected editorial photos pass at this step.
A no-mask cover signals craft. A heavily masked cover signals "we couldn't find the right photo." Try Step 2 every time before reaching for Step 3.
### Step 3 — Localized, image-toned tint (fallback only)
Only when Step 2 fails the contrast check. Three rules:
1. **Localized, not full-canvas.** Tint only the title region. Use `radial-gradient` centered on the title block, or a one-sided linear gradient that fades to fully transparent past the title. Never `inset: 0` over the whole image with a uniform alpha curve.
2. **Image-toned, not black.** Sample a dark tone already in the photo and use it as the tint color. Forest → deep moss `#1a2818`. Dusk → tea-rose dusk `#4a2638`. Snow → cool grey-blue `#2a3438`. Pure black `#000` reads as "annotation layer," not "atmospheric depth."
3. **Soft, not opaque.** Peak alpha 0.15-0.30 in the title region, falling to 0 outside. If you need >0.40 to read, the photo failed Step 1 — go back.
```css
/* Example: title in lower-left, image-toned soft tint */
.hero-bleed::after {
content: "";
position: absolute; inset: 0;
background: radial-gradient(
60% 45% at 25% 80%, /* centered on title block */
rgba(26, 40, 24, 0.28) 0%, /* forest moss tone, soft peak */
rgba(26, 40, 24, 0.0) 100%);
pointer-events: none;
}
```
### Step 4 — Thumbnail contrast check (always)
Whether or not you applied a tint:
- Render the PNG, downscale to 360 px wide in Preview / a browser tab, and look.
- Title strokes must be legible without zoom.
- If title looks like it's "fighting" the photo → swap photo or shift title to a different quiet zone, **not** strengthen the mask.
- If photo looks dead grey under uniform dark → mask is way too heavy or covers wrong area; restart from Step 1.
### Banned
- **Uniform full-canvas vertical falloff** (the `rgba(0,0,0,.55) → .10 → .10 → .80` pattern from earlier versions of this skill). That is game-key-art treatment, not editorial.
- **Pure black mask color**. Always image-toned (forest, dusk, snow, ink-blue, sepia).
- Flat black/white rectangle behind text — reads like screenshot annotation.
- `mix-blend-mode: difference` for readability — wrecks skin tones, breaks on print.
- `img { opacity: .6 }` — kills the photo's depth, the whole point of full-bleed.
- Reaching for Step 3 before honestly attempting Step 1 with a better photo.
---
## Rule 2 — Place text away from subject / face zones
Posters in 旅行 / 游戏 / 影视 / 穿搭 / 美食 frequently use a real photo as the hero. The photo has a subject — a face, a hand, a product, a peak. Text that overlaps the subject reads as graffiti, not editorial.
### Subject zone discovery — multimodal first
Before designing the title position on a hero photo, look at the image. Open it with the Read tool and observe in plain language:
1. Where is the **primary subject's face / focal feature**? (e.g. "the climber's face is in the upper-right third around 70% x 30% y")
2. Where is the **edge of the subject's silhouette**? (e.g. "left edge ends at ~40% x; below that is sky")
3. Where is the **largest open / low-detail area**? (e.g. "the lower-left quadrant is uniform fog")
Record the answers as a comment in the HTML next to the hero block:
```html
<!-- subject map (Wukong cover hero):
face/skull centerpiece: 50% x 45% y, occupies ~30% of frame
left-side staff & weapons: 15-40% x, full height
safe text zone: top band (0-15% y) and bottom band (65-100% y)
-->
```
This is the input the layout uses. It's deterministic and reviewable.
### Safe-zone placement rules
Given the subject map, place text in this order of preference:
1. **Above + below** the subject (kicker top, title bottom). 90% of full-bleed covers should do this.
2. **One side** — if subject occupies one vertical column (e.g. portrait shot on right side), text fills the opposite column. Combine with side tint from Rule 1 Step 3 only if needed.
3. **Diagonal corner** — only when the subject is in one corner and the opposite corner is genuinely empty. Rare.
Never place display titles **across** the face. A 90 px Chinese title cutting through a person's eyes is destructive even with a mask.
### Composition discipline (editorial look)
These four rules are what separates an editorial poster from a generic image-with-text:
- **Asymmetric placement.** Titles offset to one side / one corner read more confident than dead-centered ones. The Kinfolk / Cereal habit.
- **Generous negative space.** Title should occupy ≤40% of the canvas. The photo and the empty quiet zone are doing the design work.
- **Title in one quiet zone, only one.** Splitting the title into two zones (top + bottom) is fine; splitting it into three or wrapping around the subject is busy.
- **Title never overlaps the subject silhouette.** If the only safe placement requires crossing the subject's edge, switch modes (move to a frame, diptych, or off-image title) — do not enlarge the mask.
### Crop guards
The face-avoidance rule should also drive `object-position`. If the subject is in the upper third, do not use `object-position: center top` (that pushes the face into the title band).
| Subject location in raw image | Recommended `object-position` for 3:4 crop |
| ----------------------------- | ------------------------------------------ |
| Face / focus in upper third | `center 25%` (lifts face higher, gives bottom room for title) |
| Face / focus in middle third | `center center` (default; text goes top + bottom) |
| Face / focus in lower third | `center 70%` (drops face down, top room for title) |
| Wide landscape, no single subject | `center 35%` (slight upward bias to retain horizon line) |
| Vertical portrait, full body | `center top` |
### When the photo cannot accommodate text
If running the subject map shows there is no safe zone wider than ~30% of the poster (subject fills the frame, e.g. tight portrait), do not retrofit. Options:
- Move the photo into a **smaller frame** (use `.frame-img.r-3x4` or `.r-1x1` inside the layout — give it a defined slot, surround it with whitespace and text).
- **Two-image diptych** with one tight subject + one negative-space image, run titles over the negative-space one.
- **Move text off the image entirely** — title above, photo below. The Yading cover already does this.
A photo that demands the full canvas is not a sign that text should fight it. It is a sign that text should sit elsewhere.
---
## Checklist before delivery
Run this for every poster that has text touching an image:
- [ ] Photo passes quiet-zone test (≥30% canvas low-detail band) AND light test (atmospheric, not high-saturation noon).
- [ ] Tried no-mask composition first. Tint only added if Step 4 contrast check failed.
- [ ] If tinted: localized (`radial-gradient` or one-sided), image-toned color (not pure black), peak alpha ≤ 0.30.
- [ ] No full-canvas vertical falloff present (`linear-gradient(180deg, rgba(0,0,0,.X) → ...)` over `inset:0`).
- [ ] Subject map documented as a comment near the hero block.
- [ ] No display title (≥ 72 px) overlaps a face / hand / key product feature / subject silhouette.
- [ ] Title occupies ≤40% of canvas. Asymmetric placement preferred over centered.
- [ ] `object-position` chosen from the table above, not left at default `center top` for face shots.
- [ ] Thumbnail test passed (downscale to 360 px wide; title still legible without strain).
- [ ] No flat black/white text plate behind the title.
If any item fails, fix it before exporting the final PNG. Do not deliver and "we can adjust later" — the QA window for social cards is the moment of upload.
@@ -0,0 +1,831 @@
# Layout Recipes
These are static social-image recipes adapted from the Guizang PPT style language. They are not copied PPT templates.
## Editorial Magazine x E-ink Recipes
These structures (ledger / marginalia / pull-quote / photo-well / pipeline-vertical) work for **any topic** that wants a magazine-feature pace — outdoor, AI, finance, food, workplace, gaming, all welcome. The mode is a visual stance, not a content filter. See `style-system.md` "Style ↔ content type are decoupled" for the full reasoning.
Portrait rule:
- Every 3:4 page should occupy the vertical canvas intentionally. If the content only creates a thin table, switch to M08/M09/M10 below or add a large pull quote, evidence image, marginal column, or full-height ledger.
**Content density rule (hard)**: on 1080×1440 cards, content must cover ≥75% of canvas height. Any pure-whitespace band >15% canvas height (>216px) needs a stated reason — hero-image breathing, single-sentence statement, or leading/trailing margins (combined ≤15%). Do NOT use `<div style="flex: 1"></div>` to push content to the vertical centre — Editorial magazines absorb whitespace across opposing pages; social cards are scrolled one at a time and under-filled cards read as "PowerPoint with a missing element." Each recipe below carries a `Minimum density:` line indicating the smallest content set that fills a 3:4 canvas. If your copy doesn't reach that floor, **shorten the canvas (switch to 1:1) or pick a different recipe** — never just publish under-filled.
### M01 Cover: Magazine Issue Cover
Best for Rednote page 1, portrait social cards, or article-card covers.
Structure:
- Top issue row: category, date, ratio, or account label.
- Large serif/Songti-like title, usually 2-4 lines.
- One large image or photo crop occupying 35%-55% of the page.
- Bottom issue strip with 3-5 points.
Style:
- Paper background, deep ink title.
- Photo can bleed inside a large rectangular well.
- Accent appears as one vertical rule, page number, or small label.
### M02 Field Note Photo
Best for outdoor, object, hardware, or real-world observation.
Structure:
- Large documentary photo.
- Narrow caption column or bottom caption band.
- One short takeaway in large type.
Use when the photo is evidence, not decoration.
### M03 Editorial Essay Split
Best for explaining one idea with nuance.
Structure:
- Left: large title or pull quote.
- Right: 2-3 short paragraphs or numbered fragments.
- Thin rule between columns.
Keep paragraphs short. If it becomes dense, split the page.
**Minimum density (3:4)**: title + 3 short paragraphs OR title + 2 paragraphs + numbered footer list. Title alone is M04, not M03. If you have only a title + 1 paragraph, switch to M04 Pull Quote or fold a marginal column in for M11 Marginalia Essay.
### M04 Pull Quote / Thesis
Best for a core sentence or conclusion.
Structure:
- Large quote across the page.
- Small source/context row.
- Optional tiny note or issue marker.
Use to create rhythm between dense pages.
**Minimum density (3:4)**: this is the **one recipe** where ≤60% canvas content is allowed (hero statement = intentional whitespace). BUT you must add (a) source/context row 18-20px mono ≤15% from bottom, (b) date-stamp or 章号 kicker at top, (c) hairline rule above the source row. Without these three "anchor points," whitespace reads as missing content. Don't use M04 if you can't supply at least one anchor.
### M05 Checklist / Buying Guide
Best for Rednote practical content.
Structure:
- Header title.
- 4-6 rows, each with a number, item, and consequence.
- Optional small photo crop or material swatch.
Avoid generic rounded cards; use rows, rules, columns, and issue labels.
### M06 Evidence Wall
Best for multiple screenshots, references, or small images.
Structure:
- 2x2 or 3-column image grid.
- Each image has a short caption.
- One larger headline anchors the interpretation.
Use only when supplied images are readable at the final size.
### M07 Closing Note
Best for final page.
Structure:
- Big takeaway title (≤2 lines).
- **4-6 ledger items**, each 26-30px serif title + 16-18px serif sub-line ("consequence" / "reason" / "example"). Each item row should consume 100-140px vertical (border + padding + content).
- Closing block: pull-quote OR signature line OR price/CTA OR small marginalia (one of these is required).
- Small footer label.
Mood should feel like the end of a magazine feature, not a sales banner.
**Minimum density (3:4)**: title + **≥4 ledger items with sub-lines** + closing block. The previous "2-3 rules" version under-fills 3:4 by ~40%. If you genuinely only have 3 reasons, expand each to "title + 2-line consequence + example/quote" OR pair with a pull-quote closing block. **3 short ledger lines on a 3:4 canvas is a failure mode**; either expand or switch to M04 Pull Quote.
### M08 Tall Ledger
Best for lists, roles, pros/cons, gear items, product capabilities, and agent responsibilities.
Structure:
- Header title.
- 4-6 full-width rows.
- Each row minimum 118-170px on a 1080x1440 canvas.
- Left index/marginalia column, right title + consequence.
- Optional vertical accent strip or bottom issue note.
Use when a normal table would become too short. Do not let the ledger occupy only the middle third of the page.
### M09 Atmospheric Thesis
Best for sparse but important points.
Structure:
- WebGL/ink-flow background visible across the page.
- One very large thesis or quote.
- 1-2 supporting notes.
- Small issue metadata and a bottom rule.
Use this instead of forcing a small table onto a page with little content.
### M10 Evidence Feature
Best for supplied screenshots/photos.
Structure:
- Large screenshot/photo occupying 45%-65% of the vertical canvas.
- Headline and lead above or beside it.
- Bottom caption band with 2-3 takeaways.
The image is not an afterthought. If the screenshot is important, it must be big enough to inspect.
### M11 Marginalia Essay
Best for nuanced explanation with moderate text.
Structure:
- Wide editorial title.
- Main column with 2-3 paragraphs.
- Narrow marginal column containing keywords, quote fragments, or small evidence crops.
- Hairline vertical rule between columns.
Use this when M03 feels too empty but a ledger feels too mechanical.
### M12 Section Divider
Best for a mid-carousel breath between dense pages. Drop one between act 1 and act 2 of a 7-9 page Rednote set so the reader gets a beat of silence.
Structure:
- WebGL ink-flow background visible across most of the canvas (Editorial template mounts this by default).
- One mono `kicker` like `Act II` or `Part 2 of 3`.
- One large serif `h-display` naming the section (3-6 Chinese characters).
- One short subtitle in serif italic.
- Optional bottom `issue-strip` with the section's promise.
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-04">
<canvas class="mag-bg" data-bg="ink-flow"></canvas>
<div class="grain"></div>
<div class="content stack gap-4" style="justify-content:center; align-items:flex-start">
<p class="kicker">Act II · Part 2</p>
<h1 class="h-display">实测结果</h1>
<p class="h-sub">What actually happened.</p>
</div>
<div class="issue-strip">
<span>Section · Findings</span>
<span>—</span>
<span>3 pages</span>
</div>
</section>
```
Adapt for 1:1: drop the subtitle, center the `h-display`, keep the ink-flow background strong. Adapt for 21:9: shift title to the left half, let the WebGL atmosphere occupy the right half.
### M13 Hero Question
Best for the last page of a Rednote carousel or for a sharp pivot mid-set. The question is the entire page.
Structure:
- WebGL ink-flow background.
- Quiet kicker like `The Question` or `留给你的`.
- Big serif question, broken into 2-3 lines on `<br>` at semantic breakpoints (not random wraps).
- One short single-sentence prompt in `lead`, often a hint or invitation to comment.
- Minimal metadata at bottom.
**Minimum density (3:4)**: like M04, this is intentionally airy. Required anchors: top kicker (0-12% y) + bottom prompt + bottom metadata strip. The ink-flow WebGL background **must be visible** (don't let `.content` cover it). If the WebGL background is missing or the kicker is empty, the page reads as under-filled rather than meditative.
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-09">
<canvas class="mag-bg" data-bg="ink-flow"></canvas>
<div class="grain"></div>
<div class="content stack gap-4" style="justify-content:center">
<p class="kicker">The Question · 留给你的</p>
<h1 class="h-display">
下一次<br>
你会先<br>
问 AI 还是问朋友?
</h1>
<p class="lead">在评论区告诉我你的答案。</p>
</div>
<div class="issue-strip">
<span>End · Page 9</span>
<span>—</span>
<span>欢迎留言</span>
</div>
</section>
```
Title length cap: 3 lines, each <= 6 Chinese characters at default size. If the question is longer, shorten it; do not shrink the `h-display`.
### M14 Vertical Pipeline
Best for explaining a 3-5 step workflow, decision tree, or recipe. PPT's pipeline is horizontal; for portrait social cards rotate it vertical.
Structure:
- `kicker` + `h-xl` page title.
- 3-5 `.pipeline-v .step` rows. Each row has step number (mono), step title (serif), one-line description (sans).
- Hairline between steps, gap 28-36px.
- Optional small evidence image to the right of the title, or a single bottom caption.
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-06">
<canvas class="mag-bg" data-bg="ink-flow"></canvas>
<div class="grain"></div>
<div class="content stack gap-3">
<p class="kicker">Workflow · 4 步</p>
<h2 class="h-xl">我的写作流水线</h2>
<div class="pipeline-v">
<div class="step">
<div class="step-nb">01</div>
<div>
<h3 class="step-title">Draft</h3>
<p class="step-desc">用语音备忘录把想法说出来</p>
</div>
</div>
<div class="step">
<div class="step-nb">02</div>
<div>
<h3 class="step-title">Reorder</h3>
<p class="step-desc">用 AI 把段落顺序重新排</p>
</div>
</div>
<div class="step">
<div class="step-nb">03</div>
<div>
<h3 class="step-title">Polish</h3>
<p class="step-desc">人工逐句去 AI 味</p>
</div>
</div>
<div class="step">
<div class="step-nb">04</div>
<div>
<h3 class="step-title">Ship</h3>
<p class="step-desc">同时发小红书、推特、公众号</p>
</div>
</div>
</div>
</div>
</section>
```
Hard limit: 3-5 steps. If you have 6+, split into two pages or switch to M05 Checklist.
### M15 Before / After
Best for "old way vs new way", "before AI vs after AI", "rookie vs pro" comparisons. PPT's Before/After is left-right; on 3:4 portrait, stack top/bottom to avoid pinched columns.
Structure:
- `kicker` + `h-xl` page title.
- `.beforeafter` container with two `.ba-block` rows: one with class `before` (dimmed at .68 opacity), one without.
- Each block has its own kicker, mid title, and 3-4 short bullets.
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-07">
<canvas class="mag-bg" data-bg="ink-flow"></canvas>
<div class="grain"></div>
<div class="content stack gap-3">
<p class="kicker">Before · After</p>
<h2 class="h-xl">写作流的演变</h2>
<div class="beforeafter">
<div class="ba-block before">
<p class="kicker">Before · 旧</p>
<h3 class="h-md">在 Notion 里反复修</h3>
<ul class="body" style="margin:0; padding-left:1.2em">
<li>开头改了 20 遍</li>
<li>每个段落都想精雕</li>
<li>一周才发一条</li>
</ul>
</div>
<div class="ba-block">
<p class="kicker">After · 新</p>
<h3 class="h-md">先发版,再迭代</h3>
<ul class="body" style="margin:0; padding-left:1.2em">
<li>20 分钟出初稿</li>
<li>评论区做精修</li>
<li>一天发三条</li>
</ul>
</div>
</div>
</div>
</section>
```
Adapt for 21:9: switch to left/right split (use `.col-2` grid). Adapt for 1:1: usually too cramped; consider M04 Pull Quote instead.
### M16 Image-Led Cover (Full-Bleed Hero)
Best for **lifestyle, image-heavy** content where the user has 1+ excellent photos: 旅行目的地 / 户外场景 / 自家成菜 / 家居一角 / 情感场景照. Photo fills the canvas; title rides on top with restraint. This is what M01 is **not** — M01 splits the canvas into photo + text blocks. M16 lets the photo own the canvas.
**Reference look**: Kinfolk / Cereal / Apartamento / Monocle covers. Quiet photo, serif title, paper-cream text, generous negative space, asymmetric placement. **NOT** game key-art with heavy black gradient. **NOT** travel poster with chunky bold caption.
**Required**: `references/image-overlay.md` Rule 1 Steps 1-4 must all pass. The first gate (photo selection) is the hardest — most photos fail it.
### Photo qualification (gate before choosing M16)
Run both tests on the user's photo. Both must pass, or **switch to M01** (split-layout) instead of forcing M16.
1. **Quiet-zone test**: photo has a band of ≥30% canvas (full width × ≥30% height, or full height × ≥30% width) that is low-detail / out-of-focus / uniform. This is where the title will land.
2. **Light test**: photo carries atmospheric/restrained light — overcast, dawn fog, golden hour, forest understory, dusk silhouette, film softness. Reject high-saturation noon shots, on-camera flash, generic tourist snaps.
If only one passes: try M01. If neither: ask user for a different photo, or use M01 with a smaller `.frame-img` slot. **Do not "fix it with a mask."**
**Page sequence convention** (this is the "image-led" pattern):
```
P1 (M16) ← full-bleed photo + short title — the hook
P2 (S11 / M05 / M10) ← ledger / checklist / mini-data — text-heavy rest stop
P3 (M02 or M16-small) ← second photo as field-note evidence, not full-bleed
P4-N (M02 / M11) ← more field notes or marginalia essays
P_last (M07 / M04) ← closing note or pull quote
```
Never run two M16 in a row. After a full-bleed photo the eye needs text. P2 must be data/text.
**Four title-placement modes** — pick by subject map (from image-overlay.md Rule 2):
| Mode | Subject location | Title position | Tint (only if Step 4 contrast fails) |
| ---- | ---------------- | -------------- | ------------------------------------ |
| **A · 顶压底沉** (top kicker + bottom title) | Subject in middle third (face, peak, hero object), top + bottom open | Top: kicker 0-12% y. Bottom: 1-2 line title 72-92% y. | Try no tint. If needed: bottom-only radial, image-toned, peak α ≤ 0.30 over title block. |
| **B · 侧栏立柱** (vertical column) | Subject occupies a **clean** vertical column with ≥40% width safe opposite side. Skip B if subject edge is irregular (e.g. tree canopy spilling sideways) — use A or D instead. | Opposite column (~36-40% of width), stacked: kicker → title → subtitle | Try no tint. If needed: one-sided horizontal falloff `radial-gradient(45% 60% at 18% 50%, …)`, image-toned. |
| **C · 角落徽章** (corner badge) | Subject fills most of the frame; one corner is genuinely empty | Small block in the empty corner (≤35% w × ≤25% h): kicker / short title / meta | Light vignette only in that corner — never darken the whole image |
| **D · 下沉条带** (bottom ribbon) | Wide landscape / atmospheric scene / no single subject; abundant negative space at bottom | Bottom band 78-92% y: title + meta strip; align flush-left or flush-right (not centered) | Try no tint. If needed: bottom-only radial, image-toned, peak α 0.20-0.30 |
**Typography spec on 1080×1440 (Editorial × E-ink) — restrained, NOT chunky**:
| Element | Mode A | Mode B | Mode C | Mode D |
| ------- | ------ | ------ | ------ | ------ |
| Kicker | mono 20-22px, uppercase, tracking 0.18-0.22em | mono 20-22px | mono 18-20px | mono 22-24px |
| Title font | Noto Serif SC **400-500** (NOT 700-900) | same | same | same |
| Title size | **88-108px**. Default 96px for 2-line zh title. Drop to 88px if 2 lines × ≥7 chars. | **84-100px**, wrap on phrase boundary | 56-64px (NOT display) | **96-112px**, single line preferred |
| Title tracking (zh) | 0.10-0.15em | 0.10-0.15em | 0.05-0.10em | 0.12-0.18em |
| Title line-height | 1.10-1.18 | 1.10-1.18 | 1.20 | 1.05-1.10 |
| Subtitle | optional `h-sub` 28-32px italic Playfair, tracking 0 | same | omit | same |
| Issue strip | mono 18-20px bottom, uppercase, tracking 0.20-0.25em, hairline above | omit | omit | mono 20-22px, tracking 0.20-0.25em |
| Text color | `#f5f1e8` (paper-cream, NOT pure white #fff) | same | same | same |
| Hairline color | `rgba(245, 241, 232, 0.35)` (paper at 35% alpha) | same | same | same |
**Forbidden in M16 type**:
- Title weight 700+ inline (e.g. `font-weight: 800` to "make it pop"). M16 wants **regular-medium serif**, not bold.
- Pure white `#fff` text — too clinical, fights the photo's warmth. Always paper-cream.
- Title size > 120px on 1080-wide canvas. We are not making travel posters.
- Latin sans-serif as primary title (Helvetica / Inter for the headline) — M16 belongs to serif. Sans only as kicker / meta / mono caption.
- Centered title on D mode. Always flush-left or flush-right for asymmetric Kinfolk feel.
**Title-length budget** (cut copy if you bust this — never shrink type below 84px):
| Mode | Max Chinese chars | Max lines |
| ---- | ----------------- | --------- |
| A | 12 (2 × 6) or 14 (2 × 7 at 88px) | 2 |
| B | 14 (2 × 7) | 2 |
| C | 8 | 1 |
| D | 10 | 1 |
**Square (1:1, 1080×1080) and Wide (21:9, 2100×900) adaptations**:
- **1:1**: Use Mode A or Mode D only. Mode B's column squeeze, Mode C's corner are too tight on square. Shorten title to 6-10 chars.
- **21:9**: Use Mode B (left text column + right photo bleed) or Mode D (full-width bottom band). Mode A works but the bottom title competes with the WeChat caption strip — prefer D.
**HTML skeleton (Mode A — 顶压底沉 on 3:4, no mask)**:
```html
<section class="poster xhs" id="cover-image-led">
<!-- subject map (cover hero — example: Yading lake at dawn):
primary focus: snow peak at 50% x 32% y (middle band)
safe text zone: top band (0-12% y) low-detail sky,
bottom band (72-95% y) calm water reflection
quiet-zone test: PASS — bottom 30% is uniform water
light test: PASS — dawn overcast, soft saturation
-->
<div class="hero-bleed" style="background-image: url('assets/hero-yading.jpg');
background-size: cover;
background-position: center center;
position: absolute; inset: 0;"></div>
<!-- NO MASK — quiet zone already provides contrast.
Add only if Step 4 contrast check fails. -->
<div class="content" style="position: relative; height: 100%;
color: #f5f1e8; padding: 72px 80px;
display: flex; flex-direction: column;">
<p class="kicker" style="color: #f5f1e8; opacity: .85;
font-family: var(--mono); font-size: 22px;
letter-spacing: 0.22em; text-transform: uppercase; margin: 0">
Vol. 04 — 2026 — 户外
</p>
<div style="flex: 1"></div>
<h1 style="font-family: 'Noto Serif SC', serif;
font-weight: 500; font-size: 96px; line-height: 1.12;
letter-spacing: 0.12em; color: #f5f1e8;
margin: 0 0 18px 0">
在稻城<br/>看见冬天
</h1>
<div style="border-top: 1px solid rgba(245,241,232,.35);
padding-top: 14px; font-family: var(--mono); font-size: 19px;
letter-spacing: 0.22em; text-transform: uppercase;
color: #f5f1e8; opacity: .85">
DAY 1-4 · 4,200 M · −18°C
</div>
</div>
</section>
```
**HTML skeleton (Mode D — 下沉条带 on 21:9, for WeChat main cover)**:
```html
<section class="poster wide" id="wechat-21x9-led">
<!-- subject map: wide atmospheric landscape, mist over lake at dawn
quiet-zone test: PASS — entire bottom 35% is calm water + mist -->
<div class="hero-bleed" style="background-image: url('assets/hero-yading-wide.jpg');
background-size: cover; background-position: center 40%;
position: absolute; inset: 0;"></div>
<div class="content" style="position: relative; height: 100%;
color: #f5f1e8; padding: 0 96px 88px;
display: flex; flex-direction: column; justify-content: flex-end;
align-items: flex-start;">
<h1 style="font-family: 'Noto Serif SC', serif; font-weight: 500;
font-size: 108px; line-height: 1.05; letter-spacing: 0.14em;
margin: 0; color: #f5f1e8">
在稻城看见冬天
</h1>
<div style="margin-top: 18px; border-top: 1px solid rgba(245,241,232,.35);
padding-top: 14px; font-family: var(--mono); font-size: 20px;
letter-spacing: 0.24em; text-transform: uppercase">
VOL. 04 — DAY 1-4 — 4,200 M — 35MM
</div>
</div>
</section>
```
**Pitfalls**:
- Choosing M16 when the photo fails quiet-zone or light test. The cover will look bad no matter what mask you layer on. Fall back to M01.
- Putting Mode A's bottom title across someone's torso when the subject is in the lower third — re-run subject map and switch to Mode C, or change `object-position: center 70%`.
- Mode C with a heavy backdrop plate behind the corner title — defeats the point of having a great photo. Use vignette only.
- Using Mode B on a photo where the subject's silhouette is **irregular** (trees with sideways branches, scattered objects) — the "side column" won't be wide enough to be a true safe zone. Switch to Mode A or D.
- Adding a full-canvas vertical falloff "just in case." Mask-by-default kills the editorial look. The fix is photo selection, not opacity.
- Setting title weight 700+ or pure-white text — both are anti-pattern in Editorial M16.
- Running M16 on every cover. M16 is for **image-heavy lifestyle**. Product/tech/AI release covers still belong to S01 / S03 / M01.
## Swiss International Recipes
These structures (accent cover / matrix / KPI tower / h-bar / numbered statement) work for **any topic** that wants an engineered, quantified pace — software releases yes, but also travel budgets, fitness logs, recipe portions, reading stats, anything where you'd rather count it than narrate it. The mode is a visual stance, not a content filter.
### S01 Accent Cover
Best for Rednote cover.
Structure:
- Full accent or off-white background.
- Big light-weight title.
- Simple abstract system block, comparison, or two-node diagram.
- Bottom metadata strip.
Use one clear concept. No decorative blobs.
### S02 Two Signals / Comparison
Best for explaining two sources, two options, or two product directions.
Structure:
- Page title.
- Two large rectangular modules.
- One module may be ink-filled; the other paper/outlined.
- Short notes below each.
### S03 Data Layer / File Card
Best for Markdown, memory, source-of-truth, database, or state.
Structure:
- Large file-type or object block.
- List of 3-4 properties.
- Strong mono labels.
### S04 Interface / Browser Mock
Best for HTML, UI, presentation, interaction, or output layer.
Structure:
- Browser-window style frame with straight edges.
- Inside: one hero content block plus 2-3 functional modules.
- Bottom action or interaction strip.
### S05 Trap / Warning Rows
Best for problems, anti-patterns, and "do not do this" pages.
Structure:
- Big warning title.
- Three horizontal rows.
- Left mono label, right consequence.
Safety Orange works well here if the whole package uses it.
### S06 Pipeline / Architecture
Best for workflows and layered systems.
Structure:
- Three rows or columns: source, render, share.
- Each has number, label, action, and consequence.
- Use hairline boxes and neutral grey fills.
### S07 Takeaway Ledger
Best for final page.
Structure:
- Big thesis title.
- Three ledger rows with number, phrase, and compressed explanation.
- Dark or ink background can create closure.
### S08 Image Hero
Best for the WeChat 21:9 main cover and the Rednote cover when you have one strong photo or product render. The image fills the page; the title sits inside an overlay block; 3 quantified stats anchor the bottom.
Structure:
- `.chrome-min` top single row (category + date).
- `.image-hero` grid: `.hero-img-wrap` with one `.frame-img.r-21x9` photo (or `.r-3x2` on 3:4).
- `.hero-overlay-block` sitting over the upper-left of the image with `t-cat` kicker + `h-statement` title.
- `.hero-stats` row of 3 `.stat-block`s, each with a big `num` (e.g. `132K`) and a mono `lbl`.
HTML skeleton (21:9, the natural format for this recipe):
```html
<section class="poster wide" id="wechat-21x9-hero" data-accent="ikb">
<div class="content stack gap-7">
<div class="chrome-min">
<span class="t-cat">Release · 产品</span>
<span class="t-meta">2026.05 · v3.0</span>
</div>
<div class="image-hero">
<div class="hero-img-wrap">
<img src="assets/hero.jpg" alt="hero">
<div class="hero-overlay-block">
<p class="t-cat">Cover · 主封面</p>
<h1 class="h-statement">下一代<br>写作工作流</h1>
</div>
</div>
<div class="hero-stats">
<div class="stat-block">
<p class="num">132K</p>
<p class="lbl">Weekly Writers</p>
</div>
<div class="stat-block">
<p class="num">3.4×</p>
<p class="lbl">Output Speed</p>
</div>
<div class="stat-block">
<p class="num">98%</p>
<p class="lbl">Ship Rate</p>
</div>
</div>
</div>
</div>
</section>
```
Adapt for 3:4: keep the same `.image-hero` structure but swap `.r-21x9` to `.r-3x2`, drop overlay block down to 35% height. Adapt for 1:1: drop the overlay block entirely, push title above the image, keep at most 2 stats in `hero-stats`.
### S09 KPI Tower
Best for product-update pages, release notes, traffic dashboards, growth posts — anywhere 3-4 numbers need to be compared at a glance.
Structure:
- `kicker` + `h-xl` page title.
- `.kpi-tower-row` with 4 `.tower-col`s, each containing a `num`, `lbl`, and a `.bar-tower` whose height encodes the value via `style="--h:Npx"`.
- One column may carry the `.muted` modifier to drop the bar to neutral grey (use for a comparison baseline).
HTML skeleton (3:4 — template auto-collapses to 2 columns):
```html
<section class="poster xhs" id="xhs-data" data-accent="lemon-yellow">
<div class="content stack gap-7">
<p class="t-cat">Data · 半年增长</p>
<h2 class="h-xl">六个月,四组数字</h2>
<div class="kpi-tower-row">
<div class="tower-col">
<p class="num">132K</p>
<p class="lbl">Subscribers</p>
<div class="bar-tower" style="--h:320px"></div>
</div>
<div class="tower-col">
<p class="num">3.4M</p>
<p class="lbl">Reads</p>
<div class="bar-tower" style="--h:220px"></div>
</div>
<div class="tower-col">
<p class="num">68%</p>
<p class="lbl">Repeat Visits</p>
<div class="bar-tower" style="--h:160px"></div>
</div>
<div class="tower-col muted">
<p class="num">8.2%</p>
<p class="lbl">Conversion</p>
<div class="bar-tower" style="--h:120px"></div>
</div>
</div>
</div>
</section>
```
`--h` is a real height in px — pick values that proportionally encode the data. Do not invent numbers; if you only have 2 numbers, drop to 2 columns and use S03 File Card instead. Adapt for 21:9: keep 4 columns at the original width (it's where the layout shines). Adapt for 1:1: 2 columns max, taller bars.
### S10 H-Bar Chart
Best for rankings, comparisons of 5-10 items, "top N" lists, before/after pairs at scale. The bar fill encodes magnitude via `style="--w:NN%"`.
Structure:
- `kicker` + `h-xl` page title.
- `.h-bar-chart` grid with 5-10 `.bar-row`s.
- Each row: `.row-lbl` (Chinese item name), `.row-track` containing `.row-fill`, `.row-val` (mono number).
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-rank" data-accent="ikb">
<div class="content stack gap-7">
<p class="t-cat">Ranking · TOP 6</p>
<h2 class="h-xl">本月最受欢迎的工具</h2>
<div class="h-bar-chart">
<div class="bar-row">
<div class="row-lbl">Claude Code</div>
<div class="row-track"><div class="row-fill" style="--w:94%"></div></div>
<div class="row-val">94%</div>
</div>
<div class="bar-row">
<div class="row-lbl">Cursor</div>
<div class="row-track"><div class="row-fill" style="--w:78%"></div></div>
<div class="row-val">78%</div>
</div>
<div class="bar-row">
<div class="row-lbl">Linear</div>
<div class="row-track"><div class="row-fill" style="--w:62%"></div></div>
<div class="row-val">62%</div>
</div>
<div class="bar-row">
<div class="row-lbl">Raycast</div>
<div class="row-track"><div class="row-fill" style="--w:48%"></div></div>
<div class="row-val">48%</div>
</div>
<div class="bar-row">
<div class="row-lbl">Notion</div>
<div class="row-track"><div class="row-fill" style="--w:36%"></div></div>
<div class="row-val">36%</div>
</div>
<div class="bar-row">
<div class="row-lbl">Obsidian</div>
<div class="row-track"><div class="row-fill" style="--w:28%"></div></div>
<div class="row-val">28%</div>
</div>
</div>
</div>
</section>
```
Hard limits: max 6 rows on 1:1, max 10 rows on 3:4, max 8 rows on 21:9. On 3:4 the template stacks `row-lbl` above the track automatically — leave it alone. Always use real percentages or normalize the largest value to 100%; never fabricate.
### S11 Stacked Ledger
Best for shopping lists, expense rollups, agent capability inventories, or any "big number + label + icon" row stack where each item is quantified.
Structure:
- `kicker` + `h-xl` page title.
- `.stacked-ledger` containing 4-6 `.ledger-row`s.
- Each row: `.ledger-num` (large mono-feel sans number), `.ledger-lbl` (Chinese label + optional `.sub` secondary line), `.ledger-icn` (Lucide icon).
HTML skeleton (3:4):
```html
<section class="poster xhs" id="xhs-spend" data-accent="safety-orange">
<div class="content stack gap-7">
<p class="t-cat">Spend · 五月账单</p>
<h2 class="h-xl">这个月我把钱花在哪</h2>
<div class="stacked-ledger">
<div class="ledger-row">
<p class="ledger-num">¥1,280</p>
<div class="ledger-lbl">订阅 · Subscriptions
<span class="sub">Claude · Cursor · Linear</span>
</div>
<i class="ledger-icn" data-lucide="square-stack"></i>
</div>
<div class="ledger-row">
<p class="ledger-num">¥860</p>
<div class="ledger-lbl">书 · Books
<span class="sub">6 本中文 · 2 本英文</span>
</div>
<i class="ledger-icn" data-lucide="book-open"></i>
</div>
<div class="ledger-row">
<p class="ledger-num">¥540</p>
<div class="ledger-lbl">咖啡 · Coffee
<span class="sub">主要在 Manner 和 Seesaw</span>
</div>
<i class="ledger-icn" data-lucide="coffee"></i>
</div>
<div class="ledger-row">
<p class="ledger-num">¥320</p>
<div class="ledger-lbl">硬件 · Hardware
<span class="sub">键盘配件 · 一根 USB-C</span>
</div>
<i class="ledger-icn" data-lucide="keyboard"></i>
</div>
</div>
</div>
</section>
```
Hard limits: max 6 rows on 3:4, max 4 rows on 1:1, max 8 rows on 21:9. Pick angular Lucide icons (`book-open`, `coffee`, `bolt`, `square-stack`, `keyboard`) — never `heart-filled` or `smile`. If you cannot find numerical evidence, this is the wrong recipe; use S05 Trap Rows or M08 Tall Ledger.
### S12 Matrix + Hero Stat
Best for capability matrices, agent inventories, "this set covers X domains" pages where 8-12 small cells back a single bottom-line number.
Structure:
- `kicker` + `h-xl` page title.
- `.matrix-fill` grid of 8-12 `.matrix-cell`s. Each cell: `.cell-nb` (numbered mono label like `01`) and `.cell-title` (one short Chinese phrase).
- Exactly one cell may carry the `.is-accent` modifier to single out the most important capability.
- `.hero-stat-bottom` below the grid: left column with a kicker + short sentence, right column with one `num-mega` summary stat.
HTML skeleton (3:4 — matrix collapses to 2 columns):
```html
<section class="poster xhs" id="xhs-matrix" data-accent="lemon-green">
<div class="content stack gap-7">
<p class="t-cat">Capabilities · 能力清单</p>
<h2 class="h-xl">一个 Agent,十二件事</h2>
<div class="matrix-fill">
<div class="matrix-cell"><p class="cell-nb">01</p><p class="cell-title">读取项目结构</p></div>
<div class="matrix-cell"><p class="cell-nb">02</p><p class="cell-title">运行测试</p></div>
<div class="matrix-cell is-accent"><p class="cell-nb">03</p><p class="cell-title">起草 PR 描述</p></div>
<div class="matrix-cell"><p class="cell-nb">04</p><p class="cell-title">回滚错误改动</p></div>
<div class="matrix-cell"><p class="cell-nb">05</p><p class="cell-title">迁移 lockfile</p></div>
<div class="matrix-cell"><p class="cell-nb">06</p><p class="cell-title">检查类型</p></div>
<div class="matrix-cell"><p class="cell-nb">07</p><p class="cell-title">追踪 issue</p></div>
<div class="matrix-cell"><p class="cell-nb">08</p><p class="cell-title">阅读日志</p></div>
</div>
<div class="hero-stat-bottom">
<div>
<p class="t-cat">In total · 累计</p>
<p class="lead">在 6 个仓库上,持续 4 周。</p>
</div>
<p class="num-mega">12</p>
</div>
</div>
</section>
```
Hard limits: 8 cells on 3:4 (2×4), 9 cells on 1:1 (3×3), 12 cells on 21:9 (4×3). Max one `is-accent` cell — two breaks the rhythm. The `num-mega` at bottom must agree with the cell count (`12` if 12 cells, `8` if 8); never display a number unsupported by the grid.
## WeChat Adaptation
For 21:9:
- Use M01/S01 as a wide composition, not a crop.
- Put title in the left or center-left safe area.
- Use the right half for photo/object/system visual.
- Enlarge title and subtitle spacing if the center feels empty.
For 1:1:
- Distill the long title into a simple centered title.
- If the user asks for no image, use type only.
- Avoid small subtitles and avoid photos by default.
- Keep the square readable as a thumbnail: one idea, big type, generous margins.
For pair preview:
- Put the `21:9` and `1:1` compositions in one HTML preview frame.
- The preview frame is for review only; do not treat it as a third required WeChat deliverable unless requested.
@@ -0,0 +1,148 @@
# Map Component (.map-block)
Use when the content has **spatial relationships**: a travel route, store locations, walking tour, neighborhood overview, store-vs-store comparison by district, before/after relocation. Single coordinates ("我在北京") don't need a map.
Both seeds ship `.map-block` + `.map-pin` + `.map-legend` classes. The same markup works in either; only the style-locked tones differ.
## Three rendering modes
Pick mode **before** writing the markup. They are not stackable.
Default selection:
- Travel guides, walking routes, hiking, store locations, or anything the reader may use for orientation → **Mode T** Mapbox Static.
- Same need, but no Mapbox token → **Mode O** OSM static tile composite.
- Conceptual relationship map, fictional route, or purely editorial "places are connected like this" graphic → **Mode S** schematic SVG.
### Mode T · Mapbox Static Image (default for real routes)
Real raster tiles, desaturated and tinted to match the deck. Use when the content is travel / hiking / neighborhood wayfinding and you need to show actual terrain or streets. Requires `MAPBOX_ACCESS_TOKEN` in the build env.
```
https://api.mapbox.com/styles/v1/mapbox/light-v11/static/
pin-s-1+555555(116.404,39.915),
pin-l-2+1936b3(116.421,39.918)
/116.412,39.916,14,0/1200x675@2x
?access_token=YOUR_TOKEN
```
```html
<div class="map-block r-16x10 tone-paper">
<img src="https://api.mapbox.com/styles/v1/mapbox/light-v11/static/.../1200x675@2x?access_token=..." alt="Lhasa walking route">
<!-- pins overlay (calculate % from mapbox bounds) -->
<div class="map-pin" style="left:34%; top:52%;">...</div>
<div class="map-pin accent" style="left:64%; top:42%;">...</div>
<div class="map-legend">LHASA · MAPBOX LIGHT</div>
</div>
```
Editorial uses `tone-paper` (multiply-blend, saturation 36%). Swiss uses `tone-paper` with saturation 0 (pure greyscale). Don't show the raw Mapbox colour-saturated style — it fights the deck.
**Style allowlist** for the URL — keep it monochrome:
- `mapbox/light-v11`
- `mapbox/dark-v11`
- `mapbox/streets-v12` (only with `tone-paper`, never raw)
Never use `outdoors-v12` or `satellite-v9` — too colourful, will clash with both styles.
### Mode O · OpenStreetMap tile composite (fallback for real routes)
Free, no token. Use this when a real map is needed but Mapbox is unavailable. Render server-side via `staticmaps` npm package or similar, then place the raster as the `.map-block > img` child. Quality is lower than Mapbox, so keep labels sparse and let HTML pins carry the useful text.
### Mode S · Schematic SVG (conceptual / illustrative only)
Hand-drawn-style vector map drawn into a `viewBox="0 0 100 100"` SVG. Best for editorial decks where the map should feel like an illustrated guide, not a satellite capture. No external service.
```html
<div class="map-block r-16x10">
<svg viewBox="0 0 100 100" preserveAspectRatio="none">
<!-- coastline / district boundary -->
<path class="map-coast" d="M 6 18 Q 32 22 58 16 T 96 28"/>
<!-- main road -->
<path class="map-road" d="M 8 78 Q 42 62 78 70 T 96 56"/>
<!-- secondary road -->
<path class="map-road" d="M 24 14 L 38 62 L 52 86"/>
<!-- water / park polygon -->
<path class="map-water" d="M 62 8 L 92 12 L 96 36 L 70 30 Z"/>
</svg>
<!-- pins overlay: % coords relative to .map-block -->
<div class="map-pin" style="left:32%; top:48%;">
<div class="dot"></div><div class="line"></div>
<div class="card"><div class="name">大昭寺</div><span class="meta">DAY 1</span></div>
</div>
<div class="map-pin accent" style="left:62%; top:38%;">
<div class="dot"></div><div class="line"></div>
<div class="card"><div class="name">布达拉宫</div><span class="meta">DAY 2 · 18:40 SUNSET</span></div>
</div>
<div class="map-legend">LHASA · 拉萨城关 · 1:8K</div>
</div>
```
The SVG paths are stylized, not GPS-accurate. **Don't** try to trace OpenStreetMap by hand — this map is a graphic device, not a wayfinding tool. Sketch the major axes that frame the pins.
Layer order in the SVG (back to front):
1. `.map-water` polygons (rivers, lakes, parks)
2. `.map-coast` (boundary)
3. `.map-grid` (optional reference grid lines)
4. `.map-road` (transport lines)
Pins sit *outside* the SVG, as HTML siblings — they get the `.frame-shot` treatment for free.
## Hard rules
- **No pin labels on the SVG or raster tile.** Names live in `.map-pin .card`, never as SVG `<text>` or baked into the map image. Keeps font rendering consistent and avoids scaling issues.
- **Max 6 pins per board**. More than that, the cards collide. Use a relations / index card to list secondary points.
- **One accent pin maximum**. Multiple accents kill the visual hierarchy.
- **Pins must not overlap cards**. Test in browser first — if two cards are within 80 px on the long axis, alternate `.left` placement.
- **No live JavaScript map**. Social cards are static PNGs, MapLibre is wasted weight. The PPT skill has an interactive variant — don't port it here.
## Pin placement playbook
The `.map-pin` element uses `left:X%; top:Y%;` to anchor on its centre. The card grows to the right by default; add `.left` to flip:
```html
<div class="map-pin left" style="left:78%; top:30%;">
<div class="dot"></div><div class="line"></div>
<div class="card"><div class="name">车公庄</div><span class="meta">START</span></div>
</div>
```
Rule of thumb:
- Pin sits in left 60% of canvas → card opens right (default).
- Pin sits in right 60% of canvas → card opens left (`.left`).
- Two pins close to each other → alternate sides so cards don't stack.
## Recipe routing
Map block fits into existing recipes; it doesn't get its own.
| Recipe | How the map fits |
| --------------- | --------------------------------------------------------------- |
| M01 split | Top half = `.map-block r-16x9`; bottom half = explanatory text. |
| M11 marginalia | Main column = `.map-block r-3x4`; aside lists relation cards. |
| M14 pipeline | One step contains a small map (r-16x10) showing that step's location. |
| S02 two signals | One signal cell holds a map; the other holds the data table. |
| S08 image hero | Replace the hero photo with `.map-block r-16x10`. |
Don't invent a "map-only" page — the map always pairs with text that says **why** the spatial relationship matters.
## Tone tokens
| Token | Editorial behaviour | Swiss behaviour |
| ------------- | ------------------------------------------------ | --------------------------------------------- |
| (none) | Raw — only use for SVG schematic. | Raw — only use for SVG schematic. |
| `tone-paper` | Multiply blend on warm paper. Default for travel.| Hard greyscale. Default for everything. |
| `tone-ink` | Brightness 62%, saturation 18%. Dark deck pages. | Brightness 58%, saturation 0%. Dark posters. |
Tone only affects `> img` children, not the SVG schematic.
## Common mistakes
- **Pasting Google Maps screenshots** — wrong tone, has UI chrome, will look out of place. Use Mapbox Static, OSM static tiles, or a schematic when the map is conceptual.
- **Tracing a real map into SVG** — schematic should *abstract*, not replicate. If a viewer can guess the city from your SVG alone, you over-traced.
- **Tiny pin cards** — `.card .name` is 15-16 px minimum. Cut the name if it overflows; never shrink.
- **Accent on every pin** — defeats the highlight system. Pick one most-important point.
@@ -0,0 +1,122 @@
# Platform Specs
Use high-resolution exports. The user can downsample later.
## Xiaohongshu / Rednote
Default size:
- `1080 x 1440`
- Ratio: `3:4`
- Export: PNG for text-heavy images, JPEG only when photo-only and file size matters.
Safe area:
- Side margin: 72-96px.
- Top margin: 72-112px.
- Bottom margin: 80-120px.
- Keep all text and key objects inside the safe area.
Recommended set:
- 1 cover.
- 4-8 content pages.
- 1 summary or checklist page if the article benefits from it.
Naming:
```text
output/xhs-01-cover.png
output/xhs-02-<topic>.png
output/xhs-03-<topic>.png
...
```
Cover structure:
- Big title with a sharp hook.
- One strong visual: supplied image, screenshot, generated photo, or product object.
- Bottom line with 3-5 key points or a compact issue strip.
- Avoid long subtitles. Put nuance in later pages.
Content page structure:
- One idea per page.
- 12-30 Chinese characters for the title when possible.
- 2-4 short bullet fragments or one concise paragraph.
- If a page has a screenshot, reduce the text and give the screenshot enough area.
- If lower space is empty across pages, combine pages or add a functional element such as checklist, comparison row, quote, or image evidence.
## WeChat Official Account Cover Pair
Generate a pair: one `21:9` main cover and one `1:1` square cover. Build both in the same HTML file, and include a combined preview section that places them together for visual checking.
### 21:9 Main Cover
Default size:
- `2100 x 900`
- Ratio: `21:9`
Use:
- Main article cover.
- Full or near-full headline, subtitle, and visual evidence.
- Keep title readable near center-left or within a clear safe band.
- Avoid an empty middle. Increase title scale, image scale, or spacing if the center feels hollow.
### 1:1 Cover
Default size:
- `1080 x 1080`
- Ratio: `1:1`
Default style:
- Short title only, simplified from the long title.
- Big type, center aligned.
- No image unless the user explicitly asks for one.
- No subtitle by default.
- Strong contrast and enough breathing room.
Short-title rule:
- Extract the core object and action from the long title.
- Keep it to roughly 4-10 Chinese characters, or 2 short lines when needed.
- Do not squeeze the full `21:9` title into the square.
Example:
```text
Long: 开源了一个 Skill,让 AI 接管你屏幕边那张便签纸
Square: AI 接管便签纸
```
Naming:
```text
output/wechat-21x9-cover.png
output/wechat-1x1-cover.png
output/wechat-cover-pair-preview.png
```
## Screenshot And Photo Placement
For supplied screenshots/photos:
- Put originals in `assets/`.
- Use CSS `object-fit:cover` only when cropping is safe.
- Use CSS `object-fit:contain` for UI screenshots, dense text, code, tables, or app windows.
- Set `object-position` deliberately: top for long UI when top matters, center for objects, 35%-45% vertical for portraits.
- Do not pin images to the canvas edge unless the design intentionally uses bleed.
## Final Response Media
In Codex desktop, local media can be shown with absolute paths:
```markdown
![cover](/absolute/path/to/output/xhs-01-cover.png)
```
Do this for the most important images, especially the cover and any pages the user asked to inspect.
@@ -0,0 +1,85 @@
# Portrait Fill Rules
3:4 social images have much more vertical space than a horizontal PPT slide. A layout that works in 16:9 often becomes a thin strip in 3:4. Solve that at the planning stage.
## Vertical Zones
Think of a 1080x1440 Rednote image as five zones:
| Zone | Typical Height | Role |
| --- | --- | --- |
| Header/meta | 56-90px | issue label, category, page |
| Title/lead | 240-420px | hook and claim |
| Evidence/body | 520-760px | image, ledger, diagram, comparison |
| Bottom strip | 90-180px | captions, checklist, summary |
| Footer | 52-86px | issue/page metadata |
Not every page needs all five, but every page needs an intentional vertical composition.
## Underfilled Page Smells
Revise if:
- A table or ledger occupies less than 45% of the canvas height.
- The lower 25% of the page is empty without being an atmospheric hero page.
- Four list items are rendered as short rows with large blank space below.
- A screenshot is small even though it is the evidence.
- Body copy is centered in the middle with no top/bottom rhythm.
## Fixes For Thin Tables
When a table is too short:
1. Increase row height to 118-170px.
2. Add a left marginal column with large numbers, keywords, or category labels.
3. Add a pull quote column beside the table.
4. Add a top or bottom issue strip with 2-3 consequences.
5. Convert the table to M08 Tall Ledger.
6. If there are only 2-3 points, use M04 Pull Quote or M09 Atmospheric Thesis instead of a table.
## Fixes For Sparse Text
If a page has only one core sentence:
- Use M09 Atmospheric Thesis with visible WebGL/ink background.
- Make the sentence the hero, not a small paragraph.
- Add one small source/context row.
- Use dark/ink page if the package needs rhythm.
## Fixes For Screenshot Pages
If screenshot content matters:
- Give it 45%-65% of the page height.
- Put only one key sentence above it.
- Use a bottom caption band instead of a side paragraph.
- Crop carefully; preserve readable UI labels.
## Page Rhythm
For a 7-page Rednote set, use at least 5 distinct shapes:
- Cover / feature image.
- Essay split.
- Tall ledger.
- Evidence screenshot.
- Pull quote or atmospheric thesis.
- Checklist/comparison.
- Closing note.
Avoid repeating `title + lead + 3 rows` more than twice in one set.
## Minimum Occupancy
As a rough rule, the active composition should occupy at least 78% of the page height.
This can include:
- Large type.
- Images/screenshots.
- Ledger rows.
- Background atmosphere on hero pages.
- Caption bands.
- Marginalia columns.
Whitespace is good only when it is designed. Empty leftover space is not.
@@ -0,0 +1,196 @@
# Screenshot Treatment
When the user supplies an app / web / code / dashboard screenshot, **do not** drop it raw into a `.frame-img`. The aspect ratio mismatch crops UI and the harsh edge looks SaaS-y. Use `.frame-shot` instead — a sibling class added to both seed templates.
## When to reach for it
- App / Web UI capture — anything with a status bar, tab bar, toolbar, or window chrome.
- Code / terminal screenshots.
- Dashboard / chart screenshots that need to keep every label readable.
- IDE captures where text density matters more than composition.
For **photographic** content (people, scenery, products) — keep using `.frame-img`. The treatments below assume a pixel-perfect UI source where contain-fit is non-negotiable.
## Subject Prep
Before framing, decide what the actual screenshot subject is. If the source capture contains a floating modal/card over an unrelated page, desktop chrome, cropped side text, cursor trails, notification fragments, or leftover background UI, crop to the foreground window/card first and then place that cleaned subject into the stage. Do not beautify the entire raw capture when it carries accidental text or partial UI behind the subject.
Screenshot beautification must not introduce perspective, skew, rotation, or 3D tilt unless the user explicitly asks for a mockup scene. A CleanShot-style treatment is orthographic: straight subject, equal scaling, quiet background, and clear safe padding.
## Anatomy
```
.frame-shot.r-{ratio}.corners-{sq|sm|md}.shadow-{none|soft|ed}.bg-{paper|grid|dot|grey-1|ink}.inset-{none|sub|bal}
└─ <img src="…"> (object-fit: contain by default)
```
Optional wrapping:
```
.device-browser
└─ .frame-shot.r-16x10.bg-paper
└─ <img>
.device-phone
└─ .frame-shot.r-3x4.bg-paper
└─ <img>
```
## Parameters
Pick six values before writing the markup. Treat this like the M16 cover decision tree — pick once, don't fiddle mid-build.
### 1. `r-*` ratio (required, matches slot)
| Class | Use |
| ---------- | --------------------------------------------------------- |
| `r-16x10` | Default for app / web shots, looks like a real window |
| `r-16x9` | Landscape video / dashboard / wide chart |
| `r-4x3` | Classic desktop window, legacy app |
| `r-3x2` | DSLR-style — only if the source is photographic UI mockup |
| `r-1x1` | App icon / square widget |
| `r-3x4` | Mobile portrait shot (pair with `.device-phone`) |
| `r-21x9` | Multi-monitor / ultra-wide / WeChat hero |
### 2. `corners-*` (style-locked default)
- **Swiss** default: `corners-sq`. Use `corners-sm` only if the slot has surrounding shadow or chrome.
- **Editorial** default: `corners-sm` (6 px). Bump to `corners-md` (14 px) for "cutout" feel on paper.
Never go above 14 px — anything bigger reads as iOS marketing.
### 3. `shadow-*` (style-locked default)
- Swiss → `shadow-none` 90% of the time. `shadow-soft` only on screenshots that float on `bg-grid` / `bg-dot`. `shadow-ed` adds a `1px` outline as part of the shadow — reserve for hero shots.
- Editorial → `shadow-soft` on `bg-paper-2` is the warm default. `shadow-ed` for hero shots.
### 4. `bg-*` (the screenshot "stage")
| Token | Swiss role | Editorial role |
| ------------ | ------------------- | ---------------------- |
| `bg-paper` | Default plain stage | Same |
| `bg-paper-2` | n/a | Default warm stage |
| `bg-grey-1` | Default plain stage | n/a |
| `bg-grid` | Engineering / data | Field-notes engineering |
| `bg-dot` | Subtle structure | Subtle structure |
| `bg-ink` | Dark-mode UI shot | Dark-mode UI shot |
Backgrounds are **never** accent-coloured. If the screenshot needs an accent emphasis, add a `.t-cat` chip or `.kicker` next to it — don't tint the stage.
#### Solid `bg-*` vs asset `bg-asset-*`
The tokens above are **CSS-generated solids** — flat tones, fast, no asset dependency. They work, but for code / IDE / dashboard / dense-UI captures they can feel undersold. Ported from the PPT skill, both seed templates also ship 9 **real texture WebP** backgrounds under `assets/screenshot-backgrounds/`. Reach for these when the screenshot is the page's hero, when a solid stage feels too thin, or when you need the warmth/grain of a printed magazine.
Editorial assets (`assets/screenshot-backgrounds/style-a/`):
| Class | Tone | Best for |
| --------------------------- | ------------------ | ------------------------------------------------- |
| `bg-asset-dune` | Warm sand | Travel / outdoor app shots, lifestyle products |
| `bg-asset-forest-ink` | Deep forest green | Dark-mode UI on Editorial — pairs with `ink-classic` |
| `bg-asset-indigo-porcelain` | Cool porcelain | Reading / writing apps, design tools |
| `bg-asset-kraft-paper` | Kraft brown | Notes / journaling / handwritten apps |
| `bg-asset-monocle-classic` | Cream paper | Default warm hero — most magazine-like |
Swiss assets (`assets/screenshot-backgrounds/style-b/`):
| Class | Accent | Use only when current accent matches |
| -------------------------- | ----------------- | ------------------------------------------------ |
| `bg-asset-ikb-dot` | IKB Klein Blue | `data-accent="ikb"` decks only |
| `bg-asset-lemon-green-dot` | Lemon-green | `data-accent="lemon-green"` decks only |
| `bg-asset-lemon-grid` | Lemon-yellow | `data-accent="lemon"` decks only |
| `bg-asset-safety-orange` | Safety orange | `data-accent="safety-orange"` decks only |
**Rules of thumb**
- Don't mix accents — a `data-accent="ikb"` deck must not pull a `bg-asset-safety-orange` stage.
- Asset backgrounds already carry texture; don't stack `shadow-ed` on top (the `1px` outline will read as a SaaS frame). Use `shadow-soft` or `shadow-none`.
- Decide once per deck whether asset stages are part of the visual identity — don't sprinkle one asset background into a deck of solid stages, it looks like a sample card.
- These assets are crop-safe at 16:10 and 16:9. For tall `r-3x4` or square `r-1x1` slots, prefer solid `bg-*` — the texture pattern can read awkwardly when over-cropped.
**Path note**: the `.bg-asset-*` rules use `url("../assets/screenshot-backgrounds/...")` — this assumes your deck's `index.html` lives one directory below `assets/`. If you place the deck elsewhere, override these rules locally with the correct relative path.
### 5. `inset-*` (padding between shot and stage)
- `inset-none` — image fills the frame. Use when the screenshot itself already has window chrome.
- `inset-sub` (Swiss 20 px / Editorial 24 px) — default. Lets the stage breathe.
- `inset-bal` (Swiss 48 px / Editorial 56 px) — when the shot is busy and needs to feel calm.
### 6. `fit-cover` (override)
Default is `object-fit: contain` — this is the whole point of `.frame-shot`. **Only** add `.fit-cover` when:
- The slot is a hero where exact pixels of the source don't matter (e.g. a code shot used as a background pattern).
- The user explicitly says they want the shot cropped.
## Device chrome
Two wrapper classes ship with the seeds. They wrap a `.frame-shot`:
### `.device-browser`
Adds a 32–36 px chrome bar with traffic-light dots. Use for web / desktop app captures.
```html
<div class="device-browser">
<div class="frame-shot r-16x10 bg-paper inset-none">
<img src="assets/website.png" alt="Linear inbox">
</div>
</div>
```
Pair with `shadow-soft` on the screenshot itself for a desk-photo feel.
### `.device-phone`
Wraps a 3:4 or 16:10 shot in an ink-coloured bezel with 18-24 px rounded inner corners. Use for mobile captures.
```html
<div class="device-phone">
<div class="frame-shot r-3x4 bg-paper inset-none">
<img src="assets/app.png" alt="WeChat detail">
</div>
</div>
```
Don't stack `corners-md` on top of `.device-phone` — the bezel already rounds the inner shot.
## Safe-area cropping
When the user delivers a full-screen capture (1290×2796 iOS / 1080×2400 Android / 1920×1080 desktop), do **not** show the status bar, dock, or browser tab strip unless that chrome is the subject. Crop before importing:
1. Trim the iOS / Android status bar (top 47-59 px on retina).
2. Trim the home indicator / nav gesture bar (bottom 34 px on iOS).
3. For desktop: trim everything above the page content unless wrapped in `.device-browser`.
If you can't crop ahead of time, use `object-position: center 6%` to bias the visible region downward — but this is a workaround, not the preferred path.
## Style cheat-sheet
Two recipes that cover 80% of cases.
**Swiss product demo** — pure stage, no shadow:
```
.frame-shot.r-16x10.corners-sq.shadow-none.bg-grey-1.inset-bal
```
**Editorial deep-dive** — desk-photo warmth:
```
.frame-shot.r-16x10.corners-sm.shadow-soft.bg-paper-2.inset-sub
```
**Editorial hero with real texture** — magazine-grade stage:
```
.frame-shot.r-16x10.corners-sm.shadow-soft.bg-asset-monocle-classic.inset-bal
```
**Swiss hero with brand-aligned stage** — only when accent matches asset:
```
.frame-shot.r-16x10.corners-sq.shadow-none.bg-asset-ikb-dot.inset-bal
```
For comparison shots (before / after), use **the same parameters** on both — different treatment between cells reads as inconsistency, not contrast.
## Validator
`validate-social-deck.mjs` doesn't have a screenshot-specific rule (yet). The existing R1 / R2 / R5 still apply: a `.frame-shot` that overflows or pushes the footer is still a fail.
If a deck mixes `.frame-img` and `.frame-shot` on the same poster, that's usually a smell — pick one approach per poster.
@@ -0,0 +1,188 @@
# Style System
This skill extracts Guizang PPT visual principles for static social images. It does not depend on the original PPT templates.
## Shared Rules
- Content shape decides layout. Do not pick a pretty layout first and invent content to fit it.
- Use strong hierarchy: title, hook, evidence, caption, metadata.
- Use real images as evidence or atmosphere, not decoration.
- Avoid visible clutter: no random SVG circles, oval drops, blobs, bokeh, ornamental stickers, fake diagram decorations, or decorative gradients.
- Keep all pages in a package visually related through grid, typography, palette, and recurring metadata.
- Every page should have a clear focal point.
## Style ↔ content type are decoupled
The two modes below are **visual stances, not content categories**. Any topic can be rendered in either mode — what changes is the page's feel and which structural devices are available (ledger / marginalia / pull quote vs matrix / KPI tower / h-bar). Pick by editorial intent ("feature story" vs "release note" / "system explainer"), not by topic lookup. The lists below are *good fits*, not exhaustive.
## Mode A: Editorial Magazine x E-ink
Good fits: outdoor, lifestyle, reflective, humanistic, cultural, narrative — but also workplace essays, AI think-pieces, product retrospectives, anything that wants a slow magazine-feature pace.
Visual anchors:
- Serif or Songti-like display title plus quiet sans-serif body text.
- Warm paper, deep ink, refined greys, restrained color.
- WebGL or procedural ink/contour atmosphere, especially on covers, dividers, pull quotes, and sparse pages.
- Documentary photography, field-note captions, issue labels, page numbers.
- Large but purposeful whitespace.
- Fine rules, magazine columns, image captions, pull quotes, and editorial contrast.
- Subtle paper texture is allowed; it must not lower text readability.
Use one of the 6 magazine palettes in `theme-presets.md`. Do not improvise arbitrary warm paper colors. Five palettes are light (paper-and-ink); one — **Midnight Ink** — is the only sanctioned dark variant, reserved for content where the source imagery is already dark (game art, night photography, cinematic covers). Do not invent a second dark palette.
**Typography stance — "the larger, the lighter."** This rule is non-negotiable for Editorial. Display titles run at **weight 500** with **wide tracking** (`+.03em` to `+.04em`). Small text (kicker, meta, label) is the *only* place where weight 500 is paired with mono uppercase and wide letter-spacing (`+.20em` to `+.22em`). Body uses **serif-zh**, not sans. The anti-pattern — 700 to 900 weight display, sans body, negative tracking — collapses Editorial into a generic "infographic banner" look. If a page feels heavy or shouty, the title is too bold or the body switched to sans. See `local-tests/demo-showcase/editorial.html` for source-of-truth.
Layout patterns:
- Cover: big title top or left, large photo/image evidence, bottom checklist or issue-strip.
- Feature page: one large image plus a narrow column of compressed explanation.
- Checklist page: editorial header, numbered items, one supporting image crop or icon-free diagram.
- Quote/takeaway page: large pull quote with small source/context row.
- Comparison page: two-column tension, simple labels, one visual anchor.
Background rule:
- A flat paper color is not enough for this mode. Use a layered background from `background-systems.md`: paper base, subtle grain, ink wash or contour field, and optional WebGL canvas. The background should create atmosphere while keeping text readable.
## Mode B: Swiss International
Good fits: AI tools, product updates, engineering workflows, software explainers, release notes, comparison posts — but also travel ledgers, expense logs, fitness data, recipe step counts, anything that wants a engineered/quantified pace. Topic is not the gate; visual stance is.
Visual anchors:
- Inter, Helvetica Neue, Noto Sans SC feeling.
- Very light display weights for huge type; stronger weights for small labels.
- Strict 12/16-column grid, left alignment, asymmetric whitespace.
- White/off-white, black, refined greys, exactly one high-saturation accent.
- Straight modules, hairline rules, no rounded cards, no shadows, no glassmorphism.
Use one of the 4 Swiss accent palettes in `theme-presets.md`. One package gets exactly one accent.
Rules:
- Use one accent only. Do not mix blue, yellow, green, and orange.
- Avoid gradients. Use pure color blocks, hairline rules, and grid modules.
- Headings usually sit on the top-left content axis, not center, except special square covers or explicit poster compositions.
- Huge text should be lighter; small text should be heavier.
- Images are proof blocks. Use straight edges and stable aspect-ratio containers.
Typography guidance:
| Role | Size Guidance | Weight |
| --- | --- | --- |
| Main cover title | 84-128px on 1080x1440 | 200-400 |
| Page title | 52-82px | 300-500 |
| Body copy | 28-42px | 400-500 |
| Captions/meta | 20-28px | 500-650 |
Chinese titles are visually dense. Shorten first, then reduce size. Do not solve overflow by shrinking body text below readability.
## Image Rules
- Photos: crop with intent. Keep faces, hands, products, and main objects in safe areas.
- Product screenshots: preserve text; use `object-fit:contain` when detail matters.
- Hardware photos: make the object a first-viewport signal. The photo should be large enough to inspect.
- Generated images: generate only the image content, not the final poster with text.
- Do not make generated images include page chrome, titles, borders, captions, or fake UI unless required by the concept.
## Bad Smells
Revise if you see:
- A page that looks like a generic template with the article pasted in.
- Large empty lower area across multiple pages.
- Decorative shapes that do not explain anything.
- Screenshots too small to read.
- Inconsistent margins between pages.
- Blue text on black or dark photo with poor contrast.
- A 1:1 cover that is a cramped crop of a 21:9 cover.
## Style Identity Test
A poster compiles cleanly long before its style identity is right. Run this test on every page before delivering.
### Swiss identity test
A poster is Swiss only if **all four** hold:
1. Every large display title (>=72px on 1080x1440) uses a typed class from `components.md` (`.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`). Its computed `font-weight` is `<=300`.
2. No serif family is loaded in the document head. No element uses `font-family: serif` or `Noto Serif SC` / `Playfair Display`.
3. Section separators are hairline rules (1-2px) or grid gutters, not card borders + drop shadows.
4. Exactly one accent palette is in use. No mixed accents across pages.
If any one of these fails, the poster is not Swiss — it is generic editorial or "Swiss-flavored card". Fix it or relabel the package.
### Editorial identity test
A poster is Editorial only if **all three** hold:
1. The background has at least one atmosphere layer beyond a flat fill: paper grain, ink wash, frozen WebGL canvas, or contour field (see `background-systems.md`). A page-wide solid `#f3f0e8` with nothing on top is not Editorial.
2. The display title uses a serif-zh / serif-en family (`Noto Serif SC` / `Songti SC` / `Playfair Display`).
3. The poster contains at least one of: a large photo well, a serif pull-quote with em-italic English, a left/right marginalia column, or a ledger with real magazine row hierarchy.
If a poster has a serif title, mono labels everywhere, structured pill grids, and a flat paper background — it is **Swiss-with-a-serif**, not Editorial. Either add atmosphere + magazine structure, or switch the package to Swiss honestly.
## Anti-Patterns From Real Demos
These all rendered without errors and looked plausible in isolation, but failed once compared against a clean reference.
### Anti-pattern A: thick big title in Swiss
```html
<!-- WRONG: inline 700-800 weight on a huge title -->
<h1 style="font-size: 92px; font-weight: 800">如果只能留 5 个,我留这些。</h1>
<!-- RIGHT: typed class, automatic 200-300 weight -->
<h1 class="h-statement">如果只能<br>留 <em>5 个</em>,<br>我留这些。</h1>
```
"The larger, the lighter" is a hard rule. A 90px h1 at weight 700+ instantly downgrades the design from Swiss International to generic landing-page editorial. Always reach for `.h-hero` / `.h-statement` / `.h-xl` / `.num-mega`. Do not bypass them with inline `font-size` + `font-weight`.
### Anti-pattern B: Editorial without atmosphere
A page with all of the following looks Swiss-in-disguise even if you intended Editorial:
- Single flat paper color, no grain, no ink wash, no WebGL,
- Mono labels (`IBM Plex Mono uppercase`) on every kicker, foot, pill, and row label,
- A serif headline floating alone with no large image, pull quote, or marginalia column.
Fix by **one** of:
- Add the paper grain + ink-wash background per `background-systems.md`.
- Replace mono labels with serif / italic micro copy.
- Introduce a large photo or serif pull quote with column structure.
- Switch the package to Swiss honestly — the content might want Swiss.
### Anti-pattern C: footer overlap (closing essay collides with `.foot`)
When `.foot` or `.issue-strip` is positioned with `position: absolute; bottom: ...`, any content above that grows past its expected height crashes through the footer. Use one of these safe patterns instead:
```css
/* Pattern A — flex column. Foot is the last child, pushed by margin-top: auto. */
.poster .pad { display: flex; flex-direction: column; height: 100%; }
.poster .foot { margin-top: auto; }
/* Pattern B — grid with fixed footer row. */
.poster .pad { display: grid; grid-template-rows: auto 1fr auto; height: 100%; }
```
Both seed templates already use Pattern A. Preserve it. If a recipe truly needs an absolutely-positioned foot, reserve `padding-bottom: <foot-height + 24px>` on the content container.
### Anti-pattern D: full-bleed photo with title crossing the subject
The poster looks dramatic at 100% zoom and unreadable at thumbnail size. Two common subfailures:
- A hero photo covers the entire poster, the title sits directly on whatever pixel happens to be behind it. The image's own brightness varies across the canvas, so the title is legible in some bands and invisible in others.
- A mask layer is present but only on one band (bottom), while the subject's face was pushed to the top by `object-position: center top`. The display title lands across the face.
Both pass HTML linting; both fail the thumbnail readability test.
Fix by following `image-overlay.md`:
1. **Qualify the photo first.** It needs a low-detail quiet zone and restrained light. If it fails, switch to a framed-photo recipe instead of forcing text on top.
2. **Map the subject before placing the title.** Read the image, describe in plain language where the face/focal feature sits, record the subject map as an HTML comment next to the hero block. Place text only in documented safe zones.
3. **Match `object-position` to subject location** — see the table in `image-overlay.md`. Never leave it at default for face / hero shots.
4. **Thumbnail test** — render the PNG, downscale to 360 px wide, and look at the title. If it fights the photo, add a localized, image-toned tint only around the title area; if the photo looks dead, the tint is too heavy or the photo is wrong.
This anti-pattern survives almost every other check because the HTML is valid, the image loads, and the title renders. The only way to catch it is to look at the rendered output at the size readers will actually see it.
@@ -0,0 +1,208 @@
# Theme Presets
Use one theme for one image package. Do not mix palettes across pages unless the user explicitly asks for a deliberate multi-chapter system.
## Editorial Magazine x E-ink Palettes
These are adapted from the Guizang PPT electronic-magazine mode for static Rednote and WeChat images.
### Ink Classic
Use for business commentary, AI essays, product thinking, and neutral editorial posts.
```css
:root {
--paper: #f3f0e8;
--paper-2: #ebe6da;
--ink: #0a0a0b;
--muted: #68625a;
--line: rgba(10,10,11,.22);
--accent: #111111;
--accent-soft: #d8d2c6;
}
```
### Indigo Porcelain
Use for technology, research, data, AI infrastructure, and calm analytical writing.
```css
:root {
--paper: #f2f4f5;
--paper-2: #e5ebef;
--ink: #0a1f3d;
--muted: #5f6d78;
--line: rgba(10,31,61,.20);
--accent: #315d93;
--accent-soft: #d7e1ec;
}
```
### Forest Ink
Use for hiking, outdoor, nature, sustainability, personal field notes, and grounded lifestyle posts.
```css
:root {
--paper: #f5f1e8;
--paper-2: #e8dfcf;
--ink: #16251b;
--muted: #5d665d;
--line: rgba(22,37,27,.22);
--accent: #2e6b4f;
--accent-soft: #d4dfd2;
}
```
### Kraft Paper
Use for memory, craft, personal essays, old objects, creator notes, and warm low-tech topics.
```css
:root {
--paper: #eedfc7;
--paper-2: #dfc9a8;
--ink: #2a1e13;
--muted: #755f49;
--line: rgba(42,30,19,.24);
--accent: #9b5a2e;
--accent-soft: #d5b58f;
}
```
### Dune
Use for design, object studies, portfolio-like covers, gallery tone, and restrained aesthetic posts.
```css
:root {
--paper: #f0e6d2;
--paper-2: #ded0b7;
--ink: #1f1a14;
--muted: #6f6557;
--line: rgba(31,26,20,.22);
--accent: #8f7650;
--accent-soft: #d4c2a4;
}
```
### Midnight Ink
The **only** official dark Editorial palette. Use for game key art, night photography, cinematic covers, dark-themed cultural pieces — content whose source imagery is already dark and would be diminished by paper backgrounds. Do not improvise other dark palettes; if Midnight Ink does not fit, pick a different mode (Editorial dark is not a universal switch).
```css
:root {
--paper: #0e0d0c;
--paper-2: #1a1714;
--ink: #ece2cf;
--muted: #9a8c75;
--line: rgba(236,226,207,.22);
--accent: #d4a04a;
--accent-soft: #3a2a14;
}
```
Midnight Ink **must** also override two background layers — light-paper math does not carry over:
```css
[data-theme="midnight-ink"] .grain {
opacity: .26;
mix-blend-mode: screen;
background-image: radial-gradient(rgba(255,244,214,.10) 1px, transparent 1px);
}
[data-theme="midnight-ink"] .paper-wash {
background:
radial-gradient(80% 50% at 28% 16%, rgba(212,160,74,.12), transparent 64%),
radial-gradient(70% 60% at 80% 86%, rgba(60,40,20,.20), transparent 72%),
linear-gradient(180deg, rgba(236,226,207,.02), rgba(0,0,0,.32));
}
[data-theme="midnight-ink"] .frame-img {
background: #18120f;
box-shadow: 0 0 0 1px rgba(236,226,207,.10);
}
```
The seed `template-editorial-card.html` ships these overrides — just switch `data-theme` and they apply automatically.
Magazine palette rules:
- Use `--paper` as the main background and `--ink` as primary type.
- Use `--accent` sparingly: section marker, page number, pull quote rule, or one highlighted phrase.
- `--paper-2` can support photo wells, issue strips, or checklist bands.
- Light palettes (the first five): do not turn into beige-on-beige. Maintain real contrast.
- Midnight Ink: do not stack opaque cards or fills on the page. Dark Editorial relies on photo bleeds + warm gilt accent for hierarchy, not background blocks.
## Swiss International Palettes
These are adapted from the Guizang PPT Swiss mode.
### IKB Blue
Default for AI, technology, product updates, design, and engineering topics.
```css
:root {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #002FA7;
--accent-on: #ffffff;
}
```
### Lemon Yellow
Use for young, consumer, active, retail, sporty, or playful information.
```css
:root {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #FFD500;
--accent-on: #0a0a0a;
}
```
### Lemon Green
Use for ecology, future, emerging tech, health, and highlighter-like contemporary topics.
```css
:root {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #C5E803;
--accent-on: #0a0a0a;
}
```
### Safety Orange
Use for industrial, warning, urgency, risk, decision points, and sharp corrections.
```css
:root {
--paper: #fafaf8;
--ink: #0a0a0a;
--grey-1: #f0f0ee;
--grey-2: #d4d4d2;
--grey-3: #737373;
--accent: #FF6B35;
--accent-on: #ffffff;
}
```
Swiss palette rules:
- Use exactly one `--accent`.
- Do not use gradients, shadows, glass, or mixed accent colors.
- If the accent is yellow or green, text on accent must use `--accent-on: #0a0a0a`.
- Prefer pure blocks, hairline rules, and grid rhythm.
@@ -0,0 +1,128 @@
# Title Shortener (1:1 covers + cross-platform reuse)
A long title that works in a 3:4 XHS card or a 21:9 WeChat cover **does not** work in a 1:1 square. Square covers need a different sentence — shorter, blunter, with one idea hitting the viewer in the first glance. This file is the playbook for generating that second title.
It also applies whenever you reuse one piece of content across multiple platforms: XHS carousel + WeChat cover pair + Lvshu single — each surface has a different attention budget.
## Why a separate title is needed
Tested empirically across the smoke decks:
| Source title (3:4 / 21:9) | Squeezed into 1:1 verbatim | Authored short title |
| -------------------------------------------------------- | ---------------------------------- | ----------------------- |
| 第三次进山,装备比上一次轻 3.4kg | overflows at 88px, fights bleed | 装备减重 3.4kg |
| 开源了一个 Skill,让 AI 接管你屏幕边那张便签纸 | 3-line wrap, looks like a paragraph | AI 接管便签纸 |
| 当代年轻人,不为了变美,只为了不丑 | 4-line wrap on `.poster.square` | 不为变美,只为不丑 |
| 我把家从北京搬到大理后,第一次安静地写完了一本书 | overflows even at 80px | 北京到大理,写完一本书 |
The squeezed version reads as a wall of text. The authored short title reads as a poster headline.
## The 5-step extraction
When the user gives you a long title (or you wrote one for the 3:4 cover), derive the 1:1 like this:
**1. Identify the core verb.** Every good poster headline has one. `接管` / `减重` / `搬到` / `写完`. Drop modifiers, drop preambles, drop adjectival noise.
**2. Identify the core object.** What is the verb acting on? `便签纸` / `装备` / `家` / `书`. Skip qualifiers (`你屏幕边那张` → `便签纸`).
**3. Compress to 4-10 Chinese characters.** That is the readable band for 88-120px type on a 1080×1080 canvas. Outside that band, you either need 2 lines (still OK), 3+ lines (almost certainly wrong), or a typographic stunt (one giant character + meta — see Big Word recipe below).
**4. Drop English unless it carries the verb.** Mixed CJK + Latin at large size is a Swiss/Editorial trick that takes a lot of practice. For 1:1 short titles, prefer pure Chinese. Exceptions: brand names (`AI`, `Mac`, `iOS`), domain terms with no Chinese equivalent (`PPT`, `MCP`).
**5. Add a small subtitle only if needed for disambiguation.** Most 1:1 covers don't need one. If you must add it, use `.kicker` or `.t-meta` size (20-22px mono), never a second `.h-xl`.
## Patterns that work for 1:1 covers
### A · Verb + Object (single line, 4-8 chars)
```
装备减重 3.4kg
AI 接管便签纸
家搬到大理
眼线一笔到位
```
Use when the verb is concrete and the object is short. The headline does the work alone — no subtitle.
### B · Two-clause cut (comma between, 2 lines)
```
不为变美,
只为不丑
```
```
六个停靠点,
两天走完
```
Use when the title needs a contrast or a setup-payoff. Hard-break at the comma; don't let CJK auto-wrap pick the line. Each line should be ≤ 6 characters.
### C · Big Word (one giant character + meta)
```html
<h1 class="h-hero" style="font-size:520px; line-height:0.86;">家</h1>
<p class="t-meta">从北京搬到大理</p>
```
A single character can carry a cover if it's the conceptual anchor of the piece. Pair it with a 1-line meta in mono. Don't try this with 2 characters — it always looks worse than either 1 or 4+.
### D · Number-led headline
```
↓ 3.4kg
装备减重
```
```
1+1=3
共创笔记
```
When the numerical fact is the news, lead with it. Editorial uses `.num-mega` (200px) over an 56px serif subtitle; Swiss uses `.num-mega` (200px) over a 22px mono cat.
## Anti-patterns
- **Squeezing the 21:9 title.** Cover the urge to use the same exact sentence. The square needs a separate sentence.
- **Adding "如何"/"怎样"/"为什么" preambles.** Question-form titles are too long for 1:1. Use a declarative form: `怎样减重装备` → `装备减重 3.4kg`.
- **Three-line titles.** If your draft wraps to 3 lines on `.poster.square`, shorten the copy — never shrink `.h-hero` below 152px. Three lines collapses into a paragraph.
- **Adding a long subtitle.** A 1:1 cover with a 2-line subtitle reads as a flyer. If the subtitle is essential, you probably picked the wrong recipe — switch to a `.poster.xhs` 3:4 instead.
- **English-only titles when the content is Chinese.** Looks like stock template. Mix only when the English carries the actual headline (e.g. brand launch).
## Sizing on `.poster.square` (1080×1080)
Type scale defaults on square boards:
| Style | Class | Default size | Use |
| --------- | ----------- | ------------ | ---------------------------------- |
| Editorial | `.h-display`| 110px | Pattern A or B, 1-2 lines |
| Editorial | `.h-xl` | 78px | Subtitle or secondary headline |
| Swiss | `.h-hero` | 200px | Pattern A or B, 1-2 lines |
| Swiss | `.h-statement` | 160px | Same role as `.h-hero`, slightly tamer |
| Both | `.num-mega` | 200px | Pattern D leading number |
If a single-line short title is ≤ 4 Chinese chars on Swiss square, you can push `.h-hero` to 240px without overflow.
## Cross-platform pairing
When the same content runs as XHS carousel + WeChat cover pair + Lvshu single:
| Surface | Title source | Length band |
| -------------------- | ------------------------------------------- | ------------------------ |
| XHS p1 cover (3:4) | Authored long title (12-30 chars) | 1-2 lines, ≤ 9 chars/line|
| WeChat 21:9 main | Authored long title or 80% of it | 1 line, ≤ 14 chars |
| WeChat 1:1 square | Shortened per this file (4-10 chars) | 1-2 lines, ≤ 6 chars/line|
| Lvshu single (3:4) | Same as XHS p1 or shortened to 8-12 chars | 1-2 lines |
The 1:1 short title is the **shortest** of the family. If you cannot shorten further without losing meaning, the content probably needs a different framing — talk to the user.
## Workflow integration
When a user task includes "公众号封面" or "WeChat cover", you should:
1. Write the long title for 21:9 first.
2. Apply this file's 5-step extraction to derive the 1:1 short title.
3. Print both in the planning notes before rendering.
4. Render both in the same HTML file, plus a `.pair-preview` section showing them side-by-side.
If the user supplies only one title and asks for the pair, derive the short one yourself and call it out in the response (don't silently swap text — the user needs to confirm the shortening reads right).