Files
Z User e45b68bb8b 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
2026-10-27 22:50:50 +00:00

839 lines
28 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN" data-theme="ink-classic">
<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=Noto+Serif+SC:wght@400;500;700;900&family=Noto+Sans+SC:wght@300;400;500;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
/* =========================================================
Editorial Magazine × E-ink — 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 配对预览
Theme switching via <html data-theme="…">:
ink-classic | indigo-porcelain | forest-ink | kraft-paper | dune | midnight-ink
Fill posters where marked <!-- POSTERS_HERE -->.
Each poster carries one Layout Recipe (M01-M15) from
references/layout-recipes.md.
========================================================= */
/* ---------- 1. Theme tokens ---------- */
:root,
[data-theme="ink-classic"] {
--paper: #f3f0e8;
--paper-2: #ebe6da;
--ink: #0a0a0b;
--muted: #68625a;
--line: rgba(10,10,11,.22);
--accent: #111111;
--accent-soft: #d8d2c6;
--ink-rgb: 10,10,11;
--paper-rgb: 243,240,232;
--accent-rgb: 17,17,17;
}
[data-theme="indigo-porcelain"] {
--paper: #f2f4f5;
--paper-2: #e5ebef;
--ink: #0a1f3d;
--muted: #5f6d78;
--line: rgba(10,31,61,.20);
--accent: #315d93;
--accent-soft: #d7e1ec;
--ink-rgb: 10,31,61;
--paper-rgb: 242,244,245;
--accent-rgb: 49,93,147;
}
[data-theme="forest-ink"] {
--paper: #f5f1e8;
--paper-2: #e8dfcf;
--ink: #16251b;
--muted: #5d665d;
--line: rgba(22,37,27,.22);
--accent: #2e6b4f;
--accent-soft: #d4dfd2;
--ink-rgb: 22,37,27;
--paper-rgb: 245,241,232;
--accent-rgb: 46,107,79;
}
[data-theme="kraft-paper"] {
--paper: #eedfc7;
--paper-2: #dfc9a8;
--ink: #2a1e13;
--muted: #755f49;
--line: rgba(42,30,19,.24);
--accent: #9b5a2e;
--accent-soft: #d5b58f;
--ink-rgb: 42,30,19;
--paper-rgb: 238,223,199;
--accent-rgb: 155,90,46;
}
[data-theme="dune"] {
--paper: #f0e6d2;
--paper-2: #ded0b7;
--ink: #1f1a14;
--muted: #6f6557;
--line: rgba(31,26,20,.22);
--accent: #8f7650;
--accent-soft: #d4c2a4;
--ink-rgb: 31,26,20;
--paper-rgb: 240,230,210;
--accent-rgb: 143,118,80;
}
/* Midnight Ink — the ONLY official dark Editorial palette.
Use for game key art, night scenes, dark photography, cinematic covers.
Do not improvise other dark backgrounds. */
[data-theme="midnight-ink"] {
--paper: #0e0d0c;
--paper-2: #1a1714;
--ink: #ece2cf;
--muted: #9a8c75;
--line: rgba(236,226,207,.22);
--accent: #d4a04a;
--accent-soft: #3a2a14;
--ink-rgb: 236,226,207;
--paper-rgb: 14,13,12;
--accent-rgb: 212,160,74;
}
/* ---------- 2. Typography stack ---------- */
:root {
--serif-zh: "Noto Serif SC", "Songti SC", "STSong", serif;
--serif-en: "Playfair Display", "Noto Serif SC", serif;
--sans-zh: "Noto Sans SC", -apple-system, "PingFang SC", "Microsoft YaHei UI", sans-serif;
--sans-en: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Consolas, monospace;
}
/* ---------- 3. Reset & sheet ---------- */
*,*::before,*::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: #1a1a1a;
font-family: var(--sans-zh);
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;
}
/* ---------- 4. 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; }
/* Default content padding by board */
.poster.xhs .content { padding: 96px 88px; }
.poster.square .content { padding: 88px 88px; }
.poster.wide .content { padding: 88px 120px; }
.content { position: relative; width: 100%; height: 100%; z-index: 2; }
/* ---------- 5. Background layers (paper grain + WebGL) ---------- */
.mag-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.grain {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
opacity: .35;
mix-blend-mode: multiply;
background-image: radial-gradient(rgba(0,0,0,.045) 1px, transparent 1px);
background-size: 3px 3px;
}
.paper-wash {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background:
linear-gradient(180deg, rgba(var(--ink-rgb),.02), rgba(var(--ink-rgb),.05) 60%, rgba(var(--ink-rgb),.08));
}
/* Midnight Ink — flip grain to screen-mode warm specks, paper-wash to
gilt + shadow vignette. Light-paper math doesn't carry over. */
[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));
}
/* ---------- 6. Typography roles ----------
Editorial Magazine values restored from demo-showcase/editorial.html:
light weights (500 not 900), serif body italic-tolerant, wide tracking
on kicker/meta (.20-.22em), positive tracking on display (+.04em).
Each card should still INLINE-tune font-size for the specific recipe;
these defaults are intentionally restrained, not maximal. */
.kicker {
font-family: var(--mono);
font-size: 21px;
letter-spacing: .22em;
text-transform: uppercase;
color: rgba(var(--ink-rgb), .55);
margin: 0 0 18px;
}
.h-display {
font-family: var(--serif-zh);
font-weight: 500;
font-size: 124px;
line-height: 1.06;
letter-spacing: .04em;
margin: 0 0 24px;
color: var(--ink);
}
.h-xl {
font-family: var(--serif-zh);
font-weight: 500;
font-size: 88px;
line-height: 1.10;
letter-spacing: .03em;
margin: 0 0 24px;
color: var(--ink);
}
.h-md {
font-family: var(--serif-zh);
font-weight: 500;
font-size: 56px;
line-height: 1.18;
letter-spacing: .02em;
margin: 0 0 18px;
color: var(--ink);
}
.h-sub {
font-family: var(--serif-en);
font-style: italic;
font-weight: 400;
font-size: 36px;
color: var(--muted);
margin: 0 0 24px;
}
.lead {
font-family: var(--serif-zh);
font-weight: 400;
font-size: 28px;
line-height: 1.55;
color: rgba(var(--ink-rgb), .82);
margin: 0 0 24px;
}
.body {
font-family: var(--serif-zh);
font-weight: 400;
font-size: 24px;
line-height: 1.65;
color: rgba(var(--ink-rgb), .80);
margin: 0 0 18px;
}
.meta {
font-family: var(--mono);
font-size: 18px;
letter-spacing: .20em;
text-transform: uppercase;
color: rgba(var(--ink-rgb), .55);
}
.label {
font-family: var(--mono);
font-size: 18px;
letter-spacing: .20em;
text-transform: uppercase;
color: rgba(var(--ink-rgb), .55);
}
.pullquote {
font-family: var(--serif-zh);
font-style: italic;
font-weight: 500;
font-size: 64px;
line-height: 1.28;
color: var(--ink);
margin: 0;
}
/* Square boards usually need tighter scales */
.poster.square .h-display { font-size: 116px; }
.poster.square .h-xl { font-size: 78px; }
.poster.square .h-md { font-size: 48px; }
.poster.square .lead { font-size: 28px; }
.poster.square .body { font-size: 26px; }
/* Wide boards have horizontal headlines */
.poster.wide .h-display { font-size: 128px; }
.poster.wide .h-xl { font-size: 96px; }
.poster.wide .lead { font-size: 34px; }
/* ---------- 7. Issue label + page metadata ---------- */
.issue-row {
display: flex;
align-items: center;
gap: 16px;
font-family: var(--mono);
font-size: 20px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
}
.issue-row .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
.issue-strip {
position: absolute;
left: 88px;
right: 88px;
bottom: 56px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 32px;
font-family: var(--mono);
font-size: 18px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
border-top: 1px solid var(--line);
padding-top: 18px;
}
.poster.wide .issue-strip { left: 120px; right: 120px; }
.issue-strip span { white-space: nowrap; }
/* ---------- 8. Layout primitives ---------- */
.stack { display: flex; flex-direction: column; }
.gap-1 { gap: 12px; }
.gap-2 { gap: 24px; }
.gap-3 { gap: 36px; }
.gap-4 { gap: 48px; }
.gap-5 { gap: 64px; }
.row { display: flex; flex-direction: row; }
.col-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; }
.col-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 36px; }
.col-2-7-5 { display: grid; grid-template-columns: 7fr 5fr; gap: 48px; }
.col-2-8-4 { display: grid; grid-template-columns: 8fr 4fr; gap: 48px; }
.center { text-align: center; }
.grow { flex: 1; }
.pad-y-md { padding-top: 36px; padding-bottom: 36px; }
.rule { height: 1px; background: var(--line); border: 0; margin: 24px 0; }
.rule-accent { height: 2px; background: var(--accent); border: 0; margin: 24px 0; width: 96px; }
/* ---------- 9. Image containers (standard ratios) ---------- */
.frame-img {
position: relative;
overflow: hidden;
background: var(--paper-2);
display: block;
margin: 0;
}
.frame-img img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 50%;
display: block;
}
.frame-img.fit-contain img { object-fit: contain; }
[data-theme="midnight-ink"] .frame-img {
background: #18120f;
box-shadow: 0 0 0 1px rgba(236,226,207,.10);
}
.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; }
.frame-img.r-3x2 { aspect-ratio: 3 / 2; }
.img-cap {
font-family: var(--mono);
font-size: 20px;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--muted);
margin-top: 14px;
}
/* ---------- 9b. Screenshot treatment (.frame-shot) ---------- */
/* Use for UI screenshots / web captures / code shots. Replaces .frame-img
when you need rounded corners, soft shadow, padded inset, or device chrome.
Stack: .frame-shot.r-{ratio}.bg-{tone}.inset-{none|sub|bal}.shadow-{none|soft|ed} */
.frame-shot {
position: relative;
overflow: hidden;
display: block;
margin: 0;
background: var(--paper-2);
/* default corners — Editorial leans slight rounding to feel like a magazine cutout */
border-radius: 6px;
}
.frame-shot.corners-sq { border-radius: 0; }
.frame-shot.corners-sm { border-radius: 6px; }
.frame-shot.corners-md { border-radius: 14px; }
/* shadows — Editorial uses warm low-contrast drop */
.frame-shot.shadow-soft { box-shadow: 0 18px 36px -18px rgba(20,18,14,.22), 0 2px 6px rgba(20,18,14,.06); }
.frame-shot.shadow-ed { box-shadow: 0 28px 60px -24px rgba(20,18,14,.32), 0 6px 14px rgba(20,18,14,.08); }
/* background tones — programmatic, no asset deps */
.frame-shot.bg-paper { background: var(--paper); }
.frame-shot.bg-paper-2 { background: var(--paper-2); }
.frame-shot.bg-ink { background: var(--ink); }
.frame-shot.bg-grid {
background:
linear-gradient(var(--paper-2), var(--paper-2)),
repeating-linear-gradient(0deg, var(--line) 0 1px, transparent 1px 48px),
repeating-linear-gradient(90deg, var(--line) 0 1px, transparent 1px 48px);
background-blend-mode: multiply;
}
.frame-shot.bg-dot {
background-color: var(--paper-2);
background-image: radial-gradient(var(--line) 1px, transparent 1px);
background-size: 18px 18px;
}
/* asset backgrounds — ported from PPT skill, real texture for screenshot wells */
/* path is relative to the deck's index.html — keep seed and final deck in same dir, or override */
.frame-shot.bg-asset-dune { background: #d8c9a8 url("assets/screenshot-backgrounds/style-a/dune.webp") center/cover no-repeat; }
.frame-shot.bg-asset-forest-ink { background: #1f2a24 url("assets/screenshot-backgrounds/style-a/forest-ink.webp") center/cover no-repeat; }
.frame-shot.bg-asset-indigo-porcelain { background: #cfd6dd url("assets/screenshot-backgrounds/style-a/indigo-porcelain.webp") center/cover no-repeat; }
.frame-shot.bg-asset-kraft-paper { background: #c9a983 url("assets/screenshot-backgrounds/style-a/kraft-paper.webp") center/cover no-repeat; }
.frame-shot.bg-asset-monocle-classic { background: #e8e1d4 url("assets/screenshot-backgrounds/style-a/monocle-classic.webp") center/cover no-repeat; }
/* padding wraps the inner shot — content gets to "float" on the bg tone */
.frame-shot.inset-none > img,
.frame-shot.inset-none > .shot-body { padding: 0; }
.frame-shot.inset-sub { padding: 24px; }
.frame-shot.inset-bal { padding: 56px; }
/* aspect-ratio reused from .frame-img */
.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; }
/* inner img defaults to contain (screenshots must not crop UI) */
.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-frame variants (CSS-only chrome, no SVG / images) */
.device-browser {
position: relative;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
}
.device-browser::before {
content: '';
display: block;
height: 36px;
background: var(--paper-2);
border-bottom: 1px solid var(--line);
background-image:
radial-gradient(circle at 18px 18px, #f08080 5px, transparent 5.5px),
radial-gradient(circle at 38px 18px, #f5c450 5px, transparent 5.5px),
radial-gradient(circle at 58px 18px, #7ec98f 5px, transparent 5.5px);
}
.device-browser > .frame-shot,
.device-browser > img {
border-radius: 0;
display: block;
width: 100%;
}
.device-phone {
position: relative;
background: var(--ink);
border-radius: 36px;
padding: 14px;
box-shadow: 0 24px 48px -20px rgba(20,18,14,.42);
}
.device-phone > .frame-shot,
.device-phone > img {
border-radius: 24px;
display: block;
width: 100%;
}
/* ---------- 9c. Map block (.map-block) ---------- */
/* Static map container — pure CSS frame, content is either an <img> from
Mapbox Static API or an SVG schematic. Pins overlay as absolute children. */
.map-block {
position: relative;
background: var(--paper-2);
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; }
/* tint mapbox / OSM tiles to fit editorial paper tone */
.map-block.tone-paper > img { filter: saturate(.36) contrast(.92) brightness(1.04); mix-blend-mode: multiply; }
.map-block.tone-ink > img { filter: saturate(.18) brightness(.62); }
/* schematic mode: SVG drawn at viewBox 0 0 100 100, lon/lat pre-mapped to 0-100 */
.map-block > svg .map-coast { fill: none; stroke: var(--line); stroke-width: .4; }
.map-block > svg .map-road { fill: none; stroke: var(--muted); stroke-width: .28; stroke-dasharray: 1.2 .8; opacity: .65; }
.map-block > svg .map-water { fill: var(--paper-2); }
.map-block > svg .map-grid { stroke: var(--line); stroke-width: .15; opacity: .55; }
/* pins overlay — positioned by % coordinates */
.map-pin {
position: absolute;
width: 0; height: 0;
transform: translate(-50%, -50%);
}
.map-pin .dot {
position: absolute;
left: -7px; top: -7px;
width: 14px; height: 14px;
border-radius: 50%;
background: var(--ink);
border: 2px solid var(--paper);
box-shadow: 0 1px 4px rgba(20,18,14,.32);
}
.map-pin.accent .dot { background: var(--accent); }
.map-pin .line {
position: absolute;
left: 8px; top: 0;
width: 32px; height: 1px;
background: var(--ink);
opacity: .5;
}
.map-pin.left .line { left: auto; right: 8px; }
.map-pin .card {
position: absolute;
left: 44px; top: -22px;
min-width: 86px;
padding: 8px 10px;
background: rgba(245,241,232,.94);
box-shadow: 0 0 0 1px var(--line);
border-radius: 2px;
white-space: nowrap;
}
.map-pin.left .card { left: auto; right: 44px; }
.map-pin .card .name {
font-family: var(--serif-zh);
font-size: 16px;
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(--muted);
}
.map-pin.accent .card .name { color: var(--accent); }
.map-legend {
position: absolute;
left: 14px; bottom: 14px;
z-index: 3;
background: rgba(245,241,232,.92);
padding: 8px 12px;
box-shadow: 0 0 0 1px var(--line);
font-family: var(--mono);
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--muted);
}
.map-legend.tr { left: auto; right: 14px; top: 14px; bottom: auto; }
/* ---------- 10. Editorial components ---------- */
.callout {
border-left: 3px solid var(--accent);
padding: 18px 28px;
font-family: var(--serif-zh);
font-style: italic;
font-size: 36px;
line-height: 1.35;
color: var(--ink);
background: var(--paper-2);
}
.callout .callout-src {
display: block;
margin-top: 18px;
font-style: normal;
font-family: var(--mono);
font-size: 20px;
letter-spacing: .12em;
color: var(--muted);
}
.ledger { display: flex; flex-direction: column; }
.ledger-row {
display: grid;
grid-template-columns: 96px 1fr auto;
gap: 24px;
align-items: baseline;
padding: 28px 0;
border-bottom: 1px solid var(--line);
}
.ledger-row .ledger-nb {
font-family: var(--mono);
font-size: 28px;
color: var(--accent);
letter-spacing: .08em;
}
.ledger-row .ledger-title {
font-family: var(--serif-zh);
font-weight: 500;
font-size: 42px;
letter-spacing: .02em;
color: var(--ink);
}
.ledger-row .ledger-note {
font-family: var(--serif-zh);
font-weight: 400;
font-size: 22px;
line-height: 1.55;
color: rgba(var(--ink-rgb), .72);
}
.marginalia {
display: grid;
grid-template-columns: 1fr 220px;
gap: 48px;
}
.marginalia .mg-col {
border-left: 1px solid var(--line);
padding-left: 24px;
}
.marginalia .mg-col p {
font-family: var(--mono);
font-size: 20px;
letter-spacing: .04em;
color: var(--muted);
margin: 0 0 16px;
}
.pipeline-v { display: flex; flex-direction: column; gap: 28px; }
.pipeline-v .step {
display: grid;
grid-template-columns: 80px 1fr;
gap: 28px;
align-items: baseline;
padding-bottom: 24px;
border-bottom: 1px solid var(--line);
}
.pipeline-v .step:last-child { border-bottom: 0; }
.pipeline-v .step-nb {
font-family: var(--mono);
font-size: 32px;
color: var(--accent);
}
.pipeline-v .step-title {
font-family: var(--serif-zh);
font-weight: 500;
font-size: 40px;
letter-spacing: .02em;
margin: 0 0 8px;
}
.pipeline-v .step-desc {
font-family: var(--serif-zh);
font-weight: 400;
font-size: 24px;
line-height: 1.6;
color: rgba(var(--ink-rgb), .72);
margin: 0;
}
.beforeafter {
display: grid;
grid-template-rows: 1fr 1fr;
gap: 32px;
height: 100%;
}
.beforeafter .ba-block {
padding: 32px;
background: var(--paper-2);
border-left: 3px solid var(--accent);
}
.beforeafter .ba-block.before { opacity: .68; }
/* ---------- 11. Pair preview frame ---------- */
.pair-preview {
width: 2400px;
min-height: 1180px;
background: var(--paper-2);
padding: 48px;
display: grid;
grid-template-columns: 2100px 1080px;
grid-template-rows: 900px 1080px;
gap: 48px;
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-size: 20px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 14px;
}
/* ---------- 12. Per-poster mounting helpers ---------- */
.poster .corner-tl,
.poster .corner-tr,
.poster .corner-bl,
.poster .corner-br {
position: absolute;
font-family: var(--mono);
font-size: 18px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
z-index: 3;
}
.poster.xhs .corner-tl,
.poster.square .corner-tl { left: 88px; top: 56px; }
.poster.xhs .corner-tr,
.poster.square .corner-tr { right: 88px; top: 56px; }
.poster.xhs .corner-bl,
.poster.square .corner-bl { left: 88px; bottom: 56px; }
.poster.xhs .corner-br,
.poster.square .corner-br { right: 88px; bottom: 56px; }
.poster.wide .corner-tl { left: 120px; top: 56px; }
.poster.wide .corner-tr { right: 120px; top: 56px; }
.poster.wide .corner-bl { left: 120px; bottom: 56px; }
.poster.wide .corner-br { right: 120px; bottom: 56px; }
</style>
</head>
<body>
<!-- THEME_OVERRIDE
Change <html data-theme="…"> above to:
ink-classic | indigo-porcelain | forest-ink | kraft-paper | dune | midnight-ink
-->
<main class="sheet">
<!-- POSTERS_HERE -->
<!--
Replace the placeholder posters below with real content.
Each poster carries one Layout Recipe (M01-M15) 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.
-->
<!-- ============================================================
Placeholder: M01 Magazine Issue Cover (3:4)
Replace with real cover content from layout-recipes.md.
============================================================ -->
<section class="poster xhs" id="xhs-01">
<canvas class="mag-bg" data-bg="ink-flow"></canvas>
<div class="grain"></div>
<div class="content stack gap-4">
<div class="issue-row">
<span>Vol. 01</span><span class="dot"></span><span>2026.05</span>
</div>
<div class="stack gap-2">
<p class="kicker">封面 · Cover</p>
<h1 class="h-display">[标题占位]<br>替换为真实文案</h1>
<p class="h-sub">A working subtitle</p>
</div>
<figure class="frame-img r-16x10">
<img src="assets/cover.jpg" alt="cover image placeholder">
</figure>
<p class="lead">用一段克制的引语解释为什么这一组卡片值得读完。</p>
</div>
<div class="issue-strip">
<span>Issue · Magazine</span>
<span>—</span>
<span>by [作者]</span>
</div>
</section>
</main>
<!-- Magazine WebGL background (deterministic ink-flow) -->
<script src="assets/magazine-bg-webgl.js"></script>
<script>
(function () {
function cssVarRgb(name, fallback) {
var raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
if (!raw) return fallback;
var parts = raw.split(',').map(function (x) { return parseInt(x.trim(), 10); });
return parts.length === 3 && parts.every(function (n) { return !isNaN(n); }) ? parts : fallback;
}
function mountAll() {
if (!window.MagazineBg || !window.MagazineBg.mount) return;
var ink = cssVarRgb('--ink-rgb', [10, 10, 11]);
var paper = cssVarRgb('--paper-rgb', [243, 240, 232]);
var accent = cssVarRgb('--accent-rgb', [17, 17, 17]);
document.querySelectorAll('canvas.mag-bg').forEach(function (c) {
window.MagazineBg.mount(c, {
ink: ink,
paper: paper,
accent: accent,
strength: 0.32,
frozenTime: 12.5
});
});
}
if (document.readyState === 'complete') mountAll();
else window.addEventListener('load', mountAll);
})();
</script>
</body>
</html>