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

+321
View File
@@ -0,0 +1,321 @@
# Chart Design — Chart Typesetting & Anti-Stacking Rules
> Core principle: **Data-Ink Ratio** — delete every line that doesn't represent data. Then delete some more.
---
## Part 1: Anti-Stacking (Collision Prevention)
Text stacking is fundamentally "information density exceeding available physical space." Apply these dynamic degradation strategies.
### 0. Universal Anti-Overlap Pre-Check (MANDATORY)
**Before rendering ANY chart, run this pre-flight check:**
```python
# Step 1: Reserve space for labels FIRST, then draw chart
# WRONG: draw chart → add labels → discover overlap → cry
# RIGHT: measure labels → reserve space → draw chart in remaining area
# Step 2: Chart-to-text separation
CHART_TEXT_MIN_GAP = 12 # pt — minimum gap between chart edge and adjacent text
CHART_LABEL_MIN_GAP = 6 # pt — minimum gap between chart labels and chart elements
# Step 3: Label-to-label collision check
def labels_overlap(label_a, label_b):
"""Check if two label bounding boxes overlap."""
return not (label_a.right < label_b.left or
label_a.left > label_b.right or
label_a.bottom < label_b.top or
label_a.top > label_b.bottom)
# Step 4: Resolution cascade
# 1. Reposition (nudge conflicting label)
# 2. Reduce font size (min 8pt)
# 3. Remove label (replace with legend entry)
# 4. Merge small items ("Others" grouping)
```
**Matplotlib-specific anti-overlap:**
```python
# MANDATORY for all matplotlib charts
import matplotlib.pyplot as plt
from adjustText import adjust_text # pip install adjustText
# After adding text annotations:
# adjust_text(texts, arrowprops=dict(arrowstyle='-', color='gray', lw=0.5))
# For bar value labels:
fig.tight_layout(pad=2.0) # Always use generous padding
plt.subplots_adjust(bottom=0.15) # Reserve space for rotated labels
# For pie/donut:
# autopct labels: check angular distance between adjacent slices
# If angle < 15°, use external labels with leader lines
```
**Chart-to-body-text separation (ReportLab):**
```python
# MANDATORY spacers around chart flowables
story.append(Spacer(1, 24)) # 24pt gap before chart
story.append(chart_image) # Chart
story.append(Spacer(1, 8)) # 8pt gap
story.append(chart_caption) # Caption
story.append(Spacer(1, 24)) # 24pt gap after chart
# NEVER place chart without Spacer guards
```
### 1. Pie / Donut Chart — Label Collision Prevention
When slices are too small, labels MUST NOT be placed inside the arc.
#### Strategy A: Leader Lines + Y-Axis Collision Avoidance
When slice angle `< 15°` (or area share `< 5%`), force external labels:
- Draw a polyline (leader line) from the arc's outer edge to the label text
- **Y-axis anti-collision logic**: Calculate adjacent label Y-coordinates. If `Y2 - Y1 < font_height + padding`, push `Y2` downward (or pull `Y1` upward). Extend/shorten the horizontal segment of the leader line accordingly.
- Leader lines: 1pt, same color as slice at 60% opacity
#### Strategy B: "Others" Grouping (Long-Tail Merge)
Before data reaches the renderer, intercept and merge:
- Threshold: slices `< 3%` → merge into a single "其他 (Others)" slice
- If detail is needed, add a minimal table beside the chart showing the breakdown
- This prevents 5+ tiny slivers from cluttering the chart
#### Strategy C: Strip Labels, Use Rich Legend
The most premium approach — **zero text on the chart itself**:
- Pie/donut body shows only pure shapes + colors
- All names, percentages, and values are laid out in a grid-aligned legend to the right or below
- This NEVER stacks, and looks the most professional
**Priority order**: C (best) → A (good) → B (acceptable fallback)
---
### 2. Bar Chart — Label Collision Prevention
#### Strategy A: Auto-Rotate to Horizontal Bar
**Hard rule**: When X-axis label average length exceeds **5 Chinese characters** (or 10 Latin characters), automatically convert to a horizontal bar chart.
- Y-axis has unlimited downward space for labels — stacking is impossible
- This is the single most effective anti-collision measure for bar charts
#### Strategy B: Tick Thinning + Stagger
When there are many bars (e.g., 30-day trend):
- **Thinning**: Show every 2nd or 5th label (skip intermediate ticks)
- **Stagger**: Alternate labels between two rows (offset vertically)
- **Tilt (last resort)**: 45° rotation works but reduces readability in premium reports. Prefer thinning or horizontal bars.
#### Strategy C: Value Label Inside/Outside Flip
- If bar is tall enough: place value label **inside** the bar near the top (use contrasting text color)
- If bar is too short for internal label: place value **above** the bar
- If value labels would overlap between adjacent bars: show values only on the tallest/shortest bars, or use tooltip-style callout boxes
---
### 3. Line Chart — Data Point Label Prevention
Dense data points with labels on every point = visual chaos.
#### Strategy A: "First, Last, Max, Min" Rule (Data Journalism Standard)
Only auto-label **4 points** on any line:
- **Start point** (first value)
- **End point** (last value)
- **Maximum** (peak)
- **Minimum** (valley)
All other points show only the curve shape — no labels. This instantly elevates professionalism.
#### Strategy B: Callout Boxes
For points that must be highlighted:
- Don't let text sit naked on the curve
- Wrap in a rounded-corner background box (white fill, very light shadow or thin border)
- Connect to the data point with a thin needle line
- Boxes have physical boundaries → easier collision detection and displacement
---
## Part 2: Visual Refinement (Eliminating "Cheap" Aesthetics)
### 1. Axis & Grid Line Treatment
**The #1 sign of amateur charts: thick black border frames and solid grid lines.**
| Element | Rule |
|---------|------|
| Top spine | **DELETE** (unconditionally) |
| Right spine | **DELETE** (unconditionally) |
| Left Y-axis spine | Optional — can delete if values are labeled on bars directly |
| Bottom X-axis | Keep as baseline reference (thin, gray) |
| Grid lines | **Dashed only** (dotted or dashed), 0.5pt, 15-20% opacity. NEVER solid. |
| Grid lines (when values shown) | **DELETE entirely** — if bar/line values are directly labeled, grid lines are redundant |
### 2. Geometric Shape Refinement
#### Pie → Donut (Mandatory Default)
- **Always use donut (ring) charts** instead of solid pies
- Inner radius = **60–70%** of outer radius
- Center space: display the total value or core metric in large text (e.g., "100%" / "¥2.4M")
- Visual weight is lighter, information hierarchy is clearer
#### Bar Styling
| Property | Value | Why |
|----------|-------|-----|
| Bar-to-gap ratio | `1.5:1` or `2:1` | Not too thin (bamboo sticks), not too fat (no breathing room) |
| Top border-radius | `2px – 4px` | Micro-rounding removes machine harshness, adds modern UI feel |
| Bottom border-radius | `0px` | Flat base anchors to the axis |
#### Line Chart Refinement
| Property | Value | Why |
|----------|-------|-----|
| Curve type | **Smooth (Bézier/spline)** | Unless showing strictly discrete data |
| Line width | `2pt – 3pt` | Stands out against weakened grid |
| Area fill | Gradient from line color at 20% opacity → 0% opacity downward | Adds volume and depth |
| Data point markers | Small circles (3-4px radius), only on labeled points | Don't mark every point |
### 3. Legend & Text Hierarchy
#### Chart Title Layout
- **Main title**: Left-aligned above the chart, bold, 14-16pt
- **Subtitle**: Below main title, regular weight, smaller (11-12pt), describes data source/period/units
- Title and chart body must have clear visual separation (≥16px gap)
- **⚠️ Caption vs Title rule**: When the chart has an external caption (figure caption / 图注, e.g. “图 1: XXX” below the chart), **do NOT add a title inside the chart** (`ax.set_title()`). The caption already serves as the chart’s identifier — an internal title would duplicate it and waste vertical space. Only add an internal title when the chart is standalone (no caption).
#### Legend Rules
| Rule | Details |
|------|---------|
| Border | **NONE** — never put a box around the legend |
| Position | Top-left horizontal row (preferred) or directly above chart area |
| Markers | Small circles (4px) or short line segments — NOT chunky squares |
| Font size | Same as axis labels (10-12pt) |
| Spacing | Generous horizontal spacing between items (≥24px) |
---
## Part 3: Default Chart Configuration
When generating any chart (HTML/SVG for Creative pipeline, or matplotlib/ReportLab for Report pipeline), apply these defaults:
```
Chart Defaults:
axes:
top_spine: hidden
right_spine: hidden
left_spine: light_gray_or_hidden
bottom_spine: light_gray_thin
grid: dashed, 0.5pt, 20% opacity (or hidden if values labeled)
pie:
type: donut
hole_ratio: 0.65
min_slice_for_internal_label: 5%
small_slice_strategy: leader_lines # or "others_merge" or "rich_legend"
others_threshold: 3%
bar:
top_radius: 3px
bar_gap_ratio: 0.5 # gap = 50% of bar width
auto_horizontal_threshold: 5_cjk_chars # or 10 latin chars
value_label_position: auto # inside if tall, outside if short
line:
smooth: true # Bézier curve
width: 2.5pt
area_fill: gradient_20_to_0
label_strategy: first_last_max_min
point_markers: labeled_points_only
legend:
border: none
position: top_left_horizontal
marker_shape: circle_small # 4px radius
marker_size: 4px
typography:
chart_title: bold, 14-16pt, left-aligned
chart_subtitle: regular, 11-12pt, left-aligned
axis_labels: 10-12pt
value_labels: 10-12pt
legend_text: 10-12pt
```
---
## Part 4: Pipeline-Specific Notes
### Creative Pipeline (Playwright HTML/CSS)
Charts in the Creative pipeline are rendered as HTML/SVG within the blueprint's components. Apply chart rules through:
- Inline SVG with proper viewBox and text positioning
- CSS classes for axis hiding, grid styling, legend layout
- JavaScript-based collision detection for leader lines (if dynamic)
### Report Pipeline (ReportLab)
Charts in the Report pipeline use ReportLab Drawing objects or embedded matplotlib figures:
- Use matplotlib with `plt.rcParams` overrides matching the defaults above
- `ax.spines['top'].set_visible(False)`, `ax.spines['right'].set_visible(False)`
- `ax.grid(True, linestyle='--', alpha=0.2, linewidth=0.5)`
- For pie charts: `plt.pie(..., wedgeprops=dict(width=0.35))` for donut effect
- For bar charts: use `matplotlib.patches.FancyBboxPatch` or `bar(..., edgecolor='none')` with manual rounded rect patches
### Academic Pipeline (LaTeX/TikZ)
- Use `pgfplots` with similar axis/grid configuration
- `\pgfplotsset{every axis/.append style={...}}`
- Donut charts via `tikz` with arc drawing
---
## Part 5: Chart Spacing & Anti-Overlap Guarantees
### 5.1 Chart-to-Body-Text Separation
| Context | Minimum Gap | Notes |
|---------|-------------|-------|
| Chart above/below body text | 24pt | Both above and below the chart |
| Chart caption to chart | 8pt | Caption immediately below chart |
| Chart caption to next body text | 18pt | Clear separation |
| Two consecutive charts | 30pt | Prevent visual merging |
### 5.2 Legend-to-Chart Overlap Prevention
**Legend MUST NOT overlap chart data area.** This is a zero-tolerance rule.
```python
# matplotlib: move legend outside plot area when overlapping
leg = ax.legend(loc='upper left', bbox_to_anchor=(0, 1), frameon=False)
# If legend still overlaps, move to below chart:
# leg = ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.12),
# ncol=3, frameon=False)
# ALWAYS call tight_layout AFTER placing legend
fig.tight_layout()
```
### 5.3 Value Label Anti-Collision
**When value labels on adjacent bars/points overlap:**
1. **Stagger vertically** — alternate above/below the bar
2. **Rotate 45°** — angled labels take less horizontal space
3. **Show only key values** — max, min, first, last
4. **Remove all and use gridlines** — let the reader estimate from axis
### 5.4 Multi-Chart Layout in Documents
When a page contains 2+ charts:
- Each chart gets its own bounding box with explicit dimensions
- Charts must not share the same vertical space (no side-by-side unless explicitly designed)
- For side-by-side charts: use a 2-column layout with `Spacer` between columns
- Each chart’s title/subtitle/legend must be fully within its own bounding box
+384
View File
@@ -0,0 +1,384 @@
# Cover Background — Advanced Cover Background Construction Rules
> Background is the canvas behind the canvas. It should be felt, not seen.
> **⚠️ V2.1 Note:** Since all covers are now rendered via HTML/Playwright, the canonical implementation is **CSS/HTML**. The ReportLab Python examples below are kept as **algorithmic reference** (coordinates, ratios, opacity values) — translate them to equivalent CSS `background`, `linear-gradient`, `radial-gradient`, `transform`, and `clip-path` properties when implementing. The design intent and constraints (opacity limits, Z-index rules, WCAG contrast) apply regardless of rendering engine.
---
## Global Constraints
Before executing any specific background algorithm, these three iron rules must be obeyed — they are the baseline to ensure backgrounds remain subtle and never overwhelm:
### 1. Ultra-Low Contrast Rule (Opacity/Alpha Control)
All background element colors are calculated based on the underlying canvas base color:
| Base Color Type | Background Element Color | Opacity Range |
|---------|-------------|-----------|
| Light/white base | `#000000` | **2% – 5%** |
| Dark base | `#FFFFFF` | **3% – 6%** |
**Absolutely forbidden** to exceed the above opacity ranges. Background elements must be at the "barely perceptible" threshold.
### 2. Absolute Z-Axis Bottom Layer
All background drawing commands must be executed after the base solid color fill and before any text/layout content rendering.
Rendering order (bottom to top):
```
1. Page base color fill (solid rect)
2. ▶ Background element layer (all content defined in this file)
3. Foreground layout content (text, borders, geometric anchors, etc.)
```
### 3. Anti-Overflow Clipping (Clip Path)
Canvas Clip must be enabled to ensure any oversized shapes exceeding the page `(0, 0, W, H)` coordinate system do not cause abnormal PDF physical dimensions or rendering errors.
**Core principle: All calculations must be based on dynamic page dimensions (`W` = page width, `H` = page height). Hard-coding absolute pixel values is forbidden.**
---
## Module 1: Supergraphics
**Goal:** Use minimal curves or diagonal lines to break the rigidity of the rectangular frame.
### Option 1.1: Arc (The Bleeding Circle)
**Shape type**: Solid-filled circle
**Calculation logic**:
- `Radius` = `W × 1.2` to `W × 1.5` (must be larger than page width to ensure arc curvature is gentle enough)
- `Center_X` = `W × 1.0` (center placed at the rightmost page edge, or even outside the right side)
- `Center_Y` = `H × 0.8` (center biased lower)
**Visual effect**: An extremely elegant, grand arc sweeps across the lower-left corner of the page, with most of the circle body outside the page.
**ReportLab implementation notes**:
```python
c.saveState()
c.clipRect(0, 0, W, H) # Rule 3: anti-overflow
c.setFillColorRGB(0, 0, 0, 0.03) # Rule 1: ultra-low opacity
radius = W * 1.3
cx, cy = W * 1.0, H * 0.2 # ReportLab coordinate Y starts from bottom
c.circle(cx, cy, radius, fill=1, stroke=0)
c.restoreState()
```
**Playwright/HTML implementation notes**:
```html
<!-- Option 2.1: Side giant spine -->
<!-- Note: Must use JS to measure text width, dynamically adjust font-size to ensure full display -->
<div style="position:absolute; inset:0; overflow:hidden; z-index:0;">
<div id="spine-watermark" style="
position: absolute;
left: 3%;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
transform-origin: center center;
font-size: min(calc(var(--W) * 0.45), 45vw);
font-family: 'FreeSerif', sans-serif;
font-weight: 900;
color: rgba(0,0,0,0.04);
white-space: nowrap;
line-height: 1;
">2026</div>
</div>
<script>
// Adaptive: ensure rotated text does not exceed 85% of page height
const el = document.getElementById('spine-watermark');
const maxH = window.innerHeight * 0.85;
if (el.offsetWidth > maxH) {
const scale = maxH / el.offsetWidth;
el.style.fontSize = (parseFloat(getComputedStyle(el).fontSize) * scale) + 'px';
}
</script>
```
---
### Option 1.2: Sharp Angle Cut (The Angle Slash)
**Shape type**: Polygon/right trapezoid
**Calculation logic (define four vertices)**:
- `Point 1` = `(0, H × 0.7)`
- `Point 2` = `(W, H × 0.4)`
- `Point 3` = `(W, H)`
- `Point 4` = `(0, H)`
**Visual effect**: Forms a tilted geometric color block at the bottom of the page. This sharp linear cut has a strong IT/consulting/engineering industry feel.
**ReportLab implementation notes**:
```python
c.saveState()
c.clipRect(0, 0, W, H)
c.setFillColorRGB(0, 0, 0, 0.04) # Rule 1
path = c.beginPath()
# ReportLab Y starts from bottom, needs flipping
path.moveTo(0, H * 0.3) # Corresponds to doc P1: (0, H*0.7) → flipped
path.lineTo(W, H * 0.6) # Corresponds to doc P2: (W, H*0.4) → flipped
path.lineTo(W, 0) # Corresponds to doc P3: (W, H) → flipped
path.lineTo(0, 0) # Corresponds to doc P4: (0, H) → flipped
path.close()
c.drawPath(path, fill=1, stroke=0)
c.restoreState()
```
---
## Module 2: Typographic Watermarks
**Goal:** Extract very short metadata text and transform it into an architectural watermark space.
### Technical Requirements (Mandatory)
| Constraint | Rule |
|-------|------|
| **Font** | Must use sans-serif, weight must be extra-bold (Black / Heavy / Bold). Recommended: FreeSerif Bold, Noto Sans SC Heavy |
| **Font prohibition** | **Absolutely forbidden** to use thin or serif fonts for this type of watermark |
| **Character count** | Extracted string length `1–5` characters (e.g. year "2026", abbreviation "AI", "B2B") |
| **Opacity** | Follow global Rule 1 (light bg 2-5%, dark bg 3-6%) |
### Option 2.1: Side Giant Spine (Vertical Spine)
**Calculation logic**:
- `Text` = Extract year (e.g. "2026")
- ** Font Size Adaptive Algorithm (Full Text Display Iron Rule):**
1. `Max_Font_Size` = `W × 0.45` (ideal maximum)
2. Measure total text height after rotation: `Text_Width = measure(Text, Max_Font_Size)` (after 90° rotation, original width becomes vertical height)
3. Available vertical space = `H × 0.85` (leaving `H × 0.075` safety margin top and bottom)
4. If `Text_Width > available vertical space`, scale down proportionally: `Font_Size = Max_Font_Size × (available_vertical_space / Text_Width)`
5. Final `Font_Size = min(Max_Font_Size, scaled_font_size)`
- `Rotation` = Counterclockwise 90° (`-90deg`)
- `Anchor_X` = `W × 0.03` (text fully within page, flush to left but not exceeding)
- `Anchor_Y` = Vertically centered = `(H - Text_Width) / 2` (text centered after rotation)
**⚠️ Full Display Iron Rule: Background watermark text must be 100% within the visible page area. Any clipping is strictly forbidden. Reduce font size rather than truncate.**
**Visual effect**: A complete bold number watermark appears on the left side, vertically centered, becoming the visual supporting pillar. Text is fully readable.
**ReportLab implementation notes**:
```python
c.saveState()
c.clipRect(0, 0, W, H)
c.setFillColorRGB(0, 0, 0, 0.04)
# Adaptive font size: ensure full text display
max_font_size = W * 0.45
text = "2026"
text_width = c.stringWidth(text, "FreeSerif", max_font_size)
available_height = H * 0.85
if text_width > available_height:
font_size = max_font_size * (available_height / text_width)
else:
font_size = max_font_size
# Recalculate actual width for centering
actual_text_width = c.stringWidth(text, "FreeSerif", font_size)
c.setFont("FreeSerif", font_size)
# Vertically centered, fully within page
center_y = (H - actual_text_width) / 2
c.translate(W * 0.03, center_y)
c.rotate(90) # ReportLab counter-clockwise is positive
c.drawString(0, 0, text)
c.restoreState()
```
---
### Option 2.2: Bottom Full Text
**Calculation logic**:
- `Text` = Document type English initials (e.g. "REPORT")
- ** Font Size Adaptive Algorithm (Full Text Display Iron Rule):**
1. `Max_Font_Size` = `W × 0.3` (ideal maximum)
2. Measure text rendering width: `Text_Width = measure(Text, Max_Font_Size)`
3. Available horizontal space = `W × 0.90` (leaving `W × 0.05` safety margin left and right)
4. If `Text_Width > available horizontal space`, scale down proportionally: `Font_Size = Max_Font_Size × (available_horizontal_space / Text_Width)`
5. Final `Font_Size = min(Max_Font_Size, scaled_font_size)`
- `Rotation` = 0° (horizontal tiling)
- `Anchor_X` = `W × 0.05`
- `Anchor_Y` = Text baseline within the bottom safe zone of the page: `H × 0.92` (text fully displayed at page bottom, not truncated)
**⚠️ Full Display Iron Rule: Background watermark text must be 100% within the visible page area. Any clipping is strictly forbidden. Reduce font size rather than truncate.**
**Visual effect**: Text sits solidly at the bottom like a foundation, fully readable, extremely dignified. No more half-truncated text.
**ReportLab implementation notes**:
```python
c.saveState()
c.clipRect(0, 0, W, H)
c.setFillColorRGB(0, 0, 0, 0.04)
# Adaptive font size: ensure full text display
max_font_size = W * 0.3
text = "REPORT"
text_width = c.stringWidth(text, "FreeSerif", max_font_size)
available_width = W * 0.90
if text_width > available_width:
font_size = max_font_size * (available_width / text_width)
else:
font_size = max_font_size
c.setFont("FreeSerif", font_size)
# Text fully within page, baseline placed in bottom safe zone
# ascent ≈ font_size * 0.75, ensure letter tops don't exceed page
c.drawString(W * 0.05, font_size * 0.3, text) # baseline slightly above bottom edge
c.restoreState()
```
---
## Module 3: Blueprint Hairlines
**Goal:** Use ultra-thin interlacing lines to enhance the "anchoring feel" and logical rigor of foreground text.
### Technical Requirements
| Constraint | Rule |
|-------|------|
| **Line width** | `Stroke_Width = 0.5pt` (**must never exceed 1pt**) |
| **Line type** | Solid, or very closely spaced dotted line (Dotted, dash: `[1, 3]`) |
| **Opacity** | Follow global Rule 1 |
### Option 3.1: Coordinate Cross
**Calculation logic (height bound to foreground layout anchor)**:
- **Vertical line (Y-axis)**: Read the left-alignment safe boundary of foreground layout (e.g. `X = W × 0.15`). Draw a vertical line from `Y = 0` to `Y = H`
- **Horizontal line (X-axis)**: Read the baseline or top boundary of the foreground main title (e.g. `Y = H × 0.32`). Draw a horizontal line from `X = 0` to `X = W`
**Visual effect**: The entire page is divided by implicit golden ratio lines, foreground text appears to grow precisely on these coordinate axes, extremely rigorous.
**ReportLab implementation notes**:
```python
c.saveState()
c.setStrokeColorRGB(0, 0, 0, 0.04)
c.setLineWidth(0.5)
# Vertical line — align to foreground text left boundary
axis_x = W * 0.15
c.line(axis_x, 0, axis_x, H)
# Horizontal line — align to main title baseline
axis_y = H * 0.68 # ReportLab Y-flip: document H*0.32 → RL H*0.68
c.line(0, axis_y, W, axis_y)
c.restoreState()
```
**Playwright/HTML implementation notes**:
```html
<div style="position:absolute; inset:0; z-index:0; pointer-events:none;">
<!-- Vertical line -->
<div style="
position:absolute;
left: 15%;
top: 0;
width: 0.5px;
height: 100%;
background: rgba(0,0,0,0.04);
"></div>
<!-- Horizontal line -->
<div style="
position:absolute;
left: 0;
top: 32%;
width: 100%;
height: 0.5px;
background: rgba(0,0,0,0.04);
"></div>
</div>
```
---
## 🛠️ Combination & Circuit Breaker (The Combination Matrix)
To ensure diversity in auto-generated backgrounds while preventing visual chaos, the system must implement a **"background combination state machine"**.
For each PDF generation, randomly select one of the following 3 legal Recipes, **cross-boundary combinations are strictly forbidden**:
### ✅ Recipe A: Minimal Modern
**Combination**: `Option 1.1 (deep-space arc)` — **this one only, no other elements**
**Applicable scenes**: Safest, most whitespace, suitable for all types of corporate reports.
**Pairing suggestions with cover layouts**:
- Layout 1 (diagonal tension) — arc in the blank diagonal area, extremely harmonious
- Layout 2 (vertical gravity axis) — arc provides lower-left gravity
- Layout 4 (golden ratio) — arc adds breathing space to the lower whitespace area
---
### ✅ Recipe B: Engineering/Academic
**Combination**: `Option 3.1 (coordinate cross)` **+** `Option 2.1 (side giant spine)`
**Logic**: Vertical giant text interlaces with ultra-thin coordinate lines on the left, creating extreme thick-thin contrast, perfect for investment pitches and research reports.
**Line avoidance rule**: Option 3.1's ultra-thin lines should avoid crossing directly through Option 2.1 (giant text) strokes to prevent visual interference. Adjust line coordinates to avoid text areas:
- Vertical line `X` at `W × 0.15` (foreground text left-alignment line)
- Giant spine anchor `X` at `-W × 0.05` (left of vertical line, no crossing)
**Pairing suggestions with cover layouts**:
- Layout 7 (left-aligned matrix) — perfect match, left spine + coordinate lines + matrix text three-layer overlay
- Layout 2A (left-aligned vertical) — vertical line aligns with axis, doubled structural feel
- Layout 6A (side rotation decoration) — Note: 6A already has rotated year, if using Recipe B then **skip Option 2.1**, keep only Option 3.1
---
### ✅ Recipe C: Solid/Weighty
**Combination**: `Option 1.2 (sharp angle cut)` **+** `Option 2.2 (bottom full text)`
**Logic**: Bottom angular color block overlaid with fully displayed English word at the bottom, very low center of gravity, suitable for annual summaries and white papers.
**Stacking order**: Draw Option 1.2 angular color block first, then Option 2.2 bleed text (text on top of color block, but both below foreground content).
**Pairing suggestions with cover layouts**:
- Layout 4A (top suspended) — content on top, background pressed below, extreme top-bottom contrast
- Layout 1A (diagonal tension) — bottom gravity echoes lower-right text
- Layout 5A (stepped progression) — steps extend to lower-right, converging with bottom gravity
---
## 🚫 Circuit Breaker Rules (Hard Constraints)
The following combinations are **hard-forbidden**; violations are bugs:
| Forbidden Rule | Reason |
|---------|------|
| Option 2.1 + Option 2.2 together | **No dual text**. Only one giant text watermark allowed per page |
| Option 1.1 + Option 1.2 together | **No dual geometry**. Large circle + large diagonal = visual chaos |
| Option 3.1 lines crossing Option 2.x strokes | **Line isolation**. Ultra-thin lines must not cross giant text strokes; adjust coordinates to avoid |
| All three modules enabled | **Maximum two modules**. Three layers = overwhelms foreground |
| Any background element opacity > 6% | **Rule 1 violation**. Background must be subtle and barely visible |
---
## Recipe Selection Logic
When no specific recipe is specified, auto-select based on document type:
| Document Type | Recommended Recipe | Reason |
|---------|---------|------|
| Corporate reports, general docs | **A** | Safest, zero risk |
| Technical reports, investment pitches | **B** | Engineering feel, precision |
| Annual summaries, white papers | **C** | Solid, weighty |
| Creative, design | **A** | Maximum whitespace, no conflict with creative content |
| Academic papers | **B** | Structural feel matches academic tone |
| Uncertain / default | **A** | Minimal never goes wrong |
---
## Relationship with geometry.md
This file defines **page-level macro backgrounds** (Supergraphics / Watermarks / Hairlines), applied to the entire canvas.
`geometry.md` defines **local decorative anchors** (Offset Stacking / Scale Contrast / Grid Intersection), applied to specific areas.
Both can coexist, but note:
- Background layer (this file) is at the bottom
- Geometric anchors (geometry.md) are above the background layer but below foreground text
- If both are used, geometric anchors should be placed in "blank areas" of background elements to avoid visual overlap
+985
View File
@@ -0,0 +1,985 @@
# Cover Design - Cover Layout Engine Specification
> The cover is the first impression. Either skip it, or build it like architecture.
---
## ⚠️ Critical Rules (Read First)
1. **Cover is OPTIONAL.** Do NOT force a cover on documents that don't need one. When in doubt, skip.
2. **Unified cover system.** All routes (Report, Creative, Academic) use the HTML/Playwright cover system. **Template 01** is general-purpose (also covers government/bidding scenarios); **Template 07 is Crystal Blue (dark cover + light-blue body)**; **Templates 03-04** are academic-specific (dark backgrounds, scholarly typography; 04 also covers Chinese journal/keyword scenarios); **Template 06 is institutional (white bg, black border frame, structured fields)**. All routes generate covers via Playwright and merge via pypdf.
3. **Single PDF output.** Never deliver a separate cover PDF.
- **Creative route**: Cover is part of the same HTML document → single PDF output inherently.
- **Report route**: Cover PDF (Playwright) + body PDF (ReportLab) → merged via pypdf into one final PDF.
- **Academic route**: Cover PDF (Playwright) + body PDF (Tectonic) → merged via pypdf into one final PDF.
4. **Page isolation.** Cover content must NEVER share a page with TOC, body text, or any subsequent content. Report/Academic: isolation is inherent in the merge pipeline. Creative: CSS page-break enforces isolation. Cover + TOC on the same page = **critical bug**.
---
## When to Include a Cover
| Document Type | Cover Needed | Notes |
|---------------|-------------|-------|
| Formal report (annual, research, white paper) | ✅ Required | Conveys professionalism |
| Proposal / plan | ✅ Required | First impression is everything |
| Resume | ❌ Not needed | Content itself is the cover |
| Menu / flyer / card | ❌ Not needed | Single page or function-oriented |
| Invitation | ❌ Not needed | The front side IS the cover |
| Lab report / academic paper | ⚠️ Situational | Add when template requires it |
| Portfolio / lookbook | ✅ Required | Cover sets the tone |
---
# PART 0: GLOBAL ENGINE ARCHITECTURE (Mandatory)
These three architectural upgrades are **mandatory** for ALL cover rendering. They eliminate 90% of squished/misaligned/overflow bugs at the engine level.
---
## A0.0 - Global Base Parameters
**All templates MUST obey these.**
```
W = page total width
H = page total height
U = W * 0.05 # Base spacing unit (5% of page width)
# All spacing should be multiples of U
```
**Coordinate origin:** `(0, 0)` at top-left corner. Some PDF libraries (ReportLab) use bottom-left origin - invert Y axis accordingly: `y_pdf = H - y_spec`.
---
## A0.1 - Absolute Anchor Grid (Replaces Flow Layout)
**Old (DEPRECATED):** `Y = previous_element_Y + height + spacing` - if one element overflows, everything below shifts and gets crushed.
**New (MANDATORY):** Page height = 100%. Every element group gets an **absolute percentage Y-anchor**. Elements may only grow **within their own bounding box**. They NEVER push or compress other blocks.
### Implementation Rule
```
# WRONG - flow layout (banned)
y_cursor = PAGE_H - top_margin
y_cursor -= title_height
y_cursor -= gap
y_cursor -= subtitle_height # ← if title wraps, subtitle gets crushed
# RIGHT - absolute anchor grid
ANCHOR_TITLE_Y = H * 0.30
ANCHOR_SUMMARY_Y = H * 0.50
ANCHOR_META_Y = H * 0.70
ANCHOR_FOOTER_Y = H * 0.90
# Each block renders at its anchor, independent of others
```
### Bounding Box Containment
Each block has a **maximum bounding box** defined by two consecutive anchors:
- Block can grow downward within `[own_anchor_Y, next_anchor_Y - min_gap]`
- If content exceeds its bounding box → trigger overflow protection (see Part 3)
- Blocks NEVER consume space belonging to adjacent blocks
---
## A0.2 - Typography Weight & Spacing System
Use **weight**, **letter-spacing**, and **opacity** to create hierarchy - not just font size.
### Mandatory Type Roles
| Role | Size | Weight | Letter-Spacing | Line-Height | Opacity | Purpose |
|------|------|--------|----------------|-------------|---------|---------|
| **Kicker / Footer** (decorative text) | 16pt | Regular | 3pt (very wide) | - | 60% | Wide spacing + transparency makes 16pt text feel delicate and recessive |
| **Summary / Description** (summary paragraph) | 16-18pt | Regular | normal | **1.6** | 85% | **Fill visual space** - 2-4 lines of descriptive text that prevents empty covers |
| **Meta / Subtitle** (secondary text) | 20-22pt | Light / Regular | normal | 1.4 | 85% | Comfortable reading rhythm, clear secondary hierarchy |
| **Hero Title** (main title) | 32-65pt (CJK: 32-80pt) | Black / Heavy (extra bold) | normal-tight | **1.15** (multi-line) | 100% | Must create overwhelming scale contrast; visually dominates the page. CJK characters need +15-20% size to match Latin visual weight. **⚠️ For 3-line CJK titles with subtitle, max ~42pt (see S3.1 table).** |
### Data-to-Drawer Binding Rule
> **Hero Title = Company/entity name. Kicker = Report type/subtitle. Never reverse.**
When users provide structured information (company name + report name/type), they must be bound to typography drawers by these rules:
| User Data Field | Bound to Typography Role | Notes |
|-------------|-------------|------|
| **Company/entity name** (e.g. "GREENTECH") | **Hero Title** (45-65pt Heavy) | Company name is the **absolute visual center**, largest font, heaviest weight |
| **Report type/subtitle** (e.g. "2025 Annual Report Summary") | **Kicker** (16pt, letter-spacing 3pt, opacity 60%) | Report name is decorative supplementary text, placed in small text position at top-left/above title |
| **Summary/description** | **Summary** (16-18pt) | Detailed description text |
| **Date/author/version** | **Meta** (20-22pt) | Auxiliary information |
| **Document number/org signature** | **Footer** (16pt, opacity 60%) | Bottom closing |
**Mapping priority (when ambiguous):**
1. If user provides only one name → treat as company/entity name, bind to Hero Title
2. If user provides two names → shorter/more brand-like → Hero Title; longer/descriptive → Kicker or Summary
3. If user explicitly labels "title" and "subtitle" → title → Hero Title, subtitle → Kicker
4. **Never use report type names (e.g. "Annual Report", "White Paper") as Hero Title's largest text** - report type is always Kicker-level decorative text
### The Summary Block Rule (Anti-Void Iron Rule)
> Every cover MUST include a Summary/Description text block. If the user provides no summary, the system MUST auto-generate one.
**Why:** A cover with only a title and date looks barren. The Summary block physically fills 2-4 lines of space, preventing the "empty field" aesthetic.
**Auto-generation rule:** When no summary/description is provided:
```python
# Generate a default summary
if not summary_text:
summary_text = f"This report was generated by {org_name or 'the system'}, containing comprehensive data analysis and insights."
# Or in English:
# summary_text = f"This report presents comprehensive analysis and key insights prepared by {org_name or 'the organization'}."
```
**Constraints:**
- Width: template-specific (typically `W * 0.5` to `W * 0.6`)
- Lines: 2-4 lines (auto-wrap at width boundary)
- Never truncate summary - if too long, reduce to 4 lines max with `...`
### Font Weight Fallback
- If font family lacks Black/Heavy weight → use Bold + slightly larger size (+4pt)
- If font family lacks Light weight → use Regular + increased letter-spacing (+1pt)
- CJK fonts: Noto Serif SC for Regular/Light roles; for Heavy/Black → increase size by 15% to compensate for single-weight CJK fonts
- English kickers/footers: **FORCE UPPERCASE** via code (`text.upper()` / `toUpperCase()`)
---
## A0.3 - Z-Index Layer Management
All cover elements must be rendered in strict layer order. No exceptions.
| Layer | Z-Index | Contents | Rules |
|-------|---------|----------|-------|
| **Layer 0** (base) | 0 | Background fill (white / light gray) | Always rendered first; full page |
| **Layer 1** (background) | 1 | Grids, watermark letters, decorative blocks, large clipped graphics | **MUST enable clip-path** - background elements may extend beyond logical bounds but must be clipped to page physical bounds. Never let background elements inflate PDF page size. |
| **Layer 2** (structure) | 2 | Ultra-thin divider lines, sidebars, corner crop marks | Structural guides that define spatial zones |
| **Layer 3** (content) | 3 | All readable text content | Rendered last, always on top |
### Clip-Path Enforcement
```css
/* clip background overflow — ONLY on Layer 1 */
.cover-bg-layer {
position: absolute;
inset: 0;
overflow: hidden; /* MANDATORY */
z-index: 1;
}
```
> ⚠️ **Clip scope = Layer 1 ONLY.** `overflow: hidden` must ONLY be on the Layer 1 background container. Layer 2 (lines) and Layer 3 (text) containers must NOT have `overflow: hidden`.
### Anti-Clip Bug (Layer 3 Text Truncation Fix)
**Symptom:** Cover text is visible but truncated by an invisible boundary.
**Root cause:** `overflow: hidden` scope too broad, clipping text in Layer 3.
**Iron rule:**
```html
<!-- ✅ CORRECT - overflow:hidden only on Layer 1 -->
<div class="cover-layer-1" style="position:absolute; inset:0; overflow:hidden; z-index:1;">
<!-- Background decorative elements -->
</div>
<div class="cover-layer-2" style="position:absolute; inset:0; z-index:2;">
<!-- Structure lines, no overflow:hidden -->
</div>
<div class="cover-layer-3" style="position:absolute; inset:0; z-index:3;">
<!-- Text content, no overflow:hidden -->
</div>
<!-- ❌ WRONG - global overflow:hidden clips text -->
<div class="cover-container" style="overflow:hidden;">
<div class="layer-1">...</div>
<div class="layer-2">...</div>
<div class="layer-3">...</div> <!-- Text gets clipped! -->
</div>
```
### No Page Border/Frame
**Symptom:** A rectangular border appears around the entire cover page.
**Iron rule:**
```css
.cover-page {
border: none !important;
outline: none !important;
box-shadow: none !important;
}
```
### Minimum Spacing Between Decorative Lines and Text (Line-to-Text Spacing)
**Symptom:** Decorative lines on the cover (Layer 2 dividers, corner marks, sidebar edges) are flush against or overlapping with text.
**Iron rule:**
```
Minimum spacing between decorative lines and any text content = U (= W * 0.05)
i.e., at least 1 U of whitespace between line edges and text edges
```
| Line Type | Minimum Spacing | Notes |
|---------|---------|------|
| Horizontal divider | `U` above and below the line | Line must not be flush against title or body text |
| Vertical sidebar edge | `U` to the right of the line | Text inside sidebar must maintain spacing from the edge |
| Ultra-thick anchor line (Template 01) | `2 * U` to the right of the line | Thick line and title need ample breathing room |
---
# PART 1: 5 Cover Templates (Cover Rendering Specifications)
Coordinates use `W` (page width) and `H` (page height). `U = W * 0.05` (base spacing unit).
All templates inherit the A0.0-A0.3 architecture rules above.
**Each template now includes a mandatory Summary/Description drawer** to fill visual space.
---
## Template 01: HUD Data Terminal - Ultra-Thick Vertical Anchor Line
**Design intent:** A single bold vertical line on the left anchors all visual weight. The thick line eliminates left-side floating. Clean, data-driven, authoritative.
### Layer 1 - Background
- Full-page grid pattern: horizontal + vertical lines at ~50pt intervals
- Grid color: primary color at **2% opacity** (white background, nearly invisible)
- Grid line width: `0.5pt`
### Layer 2 - Structure
- **Left anchor line:** Start `(0.12*W, 0.1*H)`, End `(0.12*W, 0.9*H)`. Line width = **6pt**, primary color.
### Layer 3 - Content
**Content left edge: `X_content = 0.12*W + 30pt`** (offset from the thick line)
| Drawer | Y-Anchor | Content | Constraints |
|--------|----------|---------|-------------|
| **A - Kicker** | `0.15 * H` | Report type / subtitle (e.g. "2025 Annual Report Summary") | 16pt, Regular, letter-spacing 3pt, opacity 60%, uppercase |
| **B - Hero Title** | `0.30 * H` | **Company/entity name** (e.g. "GREENTECH") | 45-65pt (CJK: 50-80pt), Heavy. Company name is the visual center |
| **C - Summary** | `0.50 * H` | 2-3 lines descriptive text about the report | 16-18pt, Regular, line-height 1.6, opacity 85%. **Width limit: `W * 0.6`**, auto-wrap. This drawer fills the mid-page void |
| **D - Meta/Date** | `0.75 * H` | Author, org, date | 16-20pt, Regular |
### Best For
Technology reports, data analysis, dashboard summaries, technical white papers
---
---
# PART 2: TEMPLATE SELECTION GUIDE
Template selection uses a two-dimensional matrix: **Intent** (from `visual_framework.md` 5-intent system) × **Document Type**. This replaces the old "Document Tone" classification and aligns with the Intent Mapping Table in `creative-fixed-canvas.md`.
| Intent | Document Type | Recommended Templates | Default |
|--------|---------------|----------------------|---------|
| **Calm** | Healthcare / Wellness / Minimalist | 07 Crystal Blue, 01 HUD | **07** |
| **Calm** | Academic / Research | 07 Crystal Blue, 04 Academic Symmetric | **07** |
| **Tension** | Crisis / Alert / Disruption | 01 HUD, 04 Academic Symmetric | **01** |
| **Energy** | Marketing / Creative / Design | 07 Crystal Blue, 04 Academic Symmetric | **07** |
| **Energy** | Technology / Data | 01 HUD, **07 Crystal Blue** | **07** |
| **Authority** | Formal / Corporate / Financial | 07 Crystal Blue, 04 Academic Symmetric | **04** |
| **Authority** | Government / Bidding | 01 HUD, 04 Academic Symmetric, **06 Institutional** | **01** |
| **Authority** | Thesis proposal / Dissertation cover | **06 Institutional** | **06** |
| **Authority** | Luxury / Editorial | 04 Academic Symmetric, 07 Crystal Blue | **04** |
| **Warmth** | Food / Lifestyle / Home | 07 Crystal Blue, 01 HUD | **07** |
> **Legacy mapping:** "Formal/Corporate" tone → Authority intent, "Minimalist" tone → Calm intent, "Luxurious/Editorial" tone → Authority intent.
**⚠️ No Global Default.** When no specific style is explicitly requested, the LLM MUST analyze the document's content, tone, and audience to autonomously select the most fitting template. Cross-reference the Intent (derived from content via `design_engine.py derive` or manual judgment) with the Document Type to find the best match. Every cover selection must be a deliberate design decision.
---
# PART 3: CODE-LEVEL SAFETY MEASURES (Pre-Render Safeguards)
These checks run BEFORE final rendering. They are **mandatory** - not optional optimizations.
---
## S3.0 - Cover Overlap Validation (MANDATORY)
**After generating the cover HTML and before rendering to PDF, run:**
```bash
node "$PDF_SKILL_DIR/scripts/cover_validate.js" cover.html
```
This detects text-vs-decorative-line overlaps by rendering the HTML and measuring actual bounding boxes. Exit code 1 = overlap found, must fix before generating the PDF.
The minimum gap between any text element and any decorative line is **1U (= 5% of page width ≈ 40px on A4)**. This catches the exact bug shown in the "text overlapping decorative lines" screenshots.
**If the check fails:**
1. Adjust the decorative line's Y position to maintain ≥ 1U gap from the nearest text
2. Or adjust the text block's position/size to avoid the overlap
3. Re-run `cover_validate.js` until it passes (exit code 0)
---
## S3.1 - Hero Title Overflow Protection (Title Line-Wrapping)
**Rule:** Hero title must NEVER exceed its template's width boundary.
1. If rendered width > max_width → word-wrap (CJK: any character; Latin: space/hyphen)
2. Multi-line hero titles: **lock line-height to 1.15**
3. Maximum lines: **3** — if 4+ lines needed, reduce font size by 4pt increments
4. Minimum font size floor: **32pt** (below this, truncate with `...`)
### Title Size vs Lines Quick-Reference (Template 01, zone B→C ≈ 225px)
| Lines | With subtitle | Without subtitle |
|-------|--------------|------------------|
| 1 | ≤ 80pt | ≤ 80pt |
| 2 | ≤ 55pt | ≤ 70pt |
| 3 | ≤ 42pt | ≤ 48pt |
**⚠️ CJK 3-line titles must reduce to ~42pt.** Do NOT use 50-80pt for 3-line CJK titles — it WILL overlap the Summary zone.
---
## S3.2 - Zone Collision Detection
After placing each text block, check if its bottom edge penetrates the next zone's top:
1. **Reposition first:** Push the collided zone down by `overlap + 20px` gap. All downstream zones shift together.
2. **Font reduction (if reposition insufficient):** Decrease title by 2pt increments. Floor: **32pt** for titles, 14pt for meta/summary.
3. **Truncation (last resort):** If font reduction hits floor and still collides, truncate with `...`
---
## S3.3 - Uppercase Lock
**Force-uppercase when content is English/Latin:**
- Kicker (category label / lead-in text)
- Footer (closing date / document number)
- Background watermark text
- Any Layer 1 decorative text
CJK text is exempt. Mixed CJK+Latin strings: uppercase only the Latin portions. Use `text-transform: uppercase` in CSS or `.upper()` in code.
---
## S3.4 - Hard Width Boundary Enforcement
**Every drawer/zone has a maximum width. Text MUST respect this width exactly.** Use CSS `max-width` on text containers.
Vertical growth (more lines) is acceptable. Horizontal overflow is a **critical bug**.
---
## S3.5 - Mandatory Summary Auto-Generation
**If the user provides only a title and no description/summary, auto-generate a 2-4 line placeholder.** A title-only cover looks barren. The Summary drawer physically fills mid-page void.
---
## S3.6 - Background Watermark Full-Display Enforcement
**All watermark text in Layer 1 must be 100% within the visible page area. Cropping is strictly forbidden.**
**Applicable scope:**
- `cover-backgrounds.md` Recipe 2.1 giant sidebar pillar
- `cover-backgrounds.md` Recipe 2.2 bottom full-size text
- Any other decorative text in the background layer
**Rules:**
1. Horizontal text: rendered width must not exceed `W * 0.90` (5% safety margin each side)
2. Vertical/rotated text: rendered height must not exceed `H * 0.85` (7.5% safety margin top/bottom)
3. If exceeded, scale down font size proportionally — never truncate
4. Anchor coordinates must never exceed page boundaries
---
## S3.7 - Line-Length Alignment (Line Must Match Text Span)
**Decorative lines must be sized relative to the text they serve:**
- **Vertical lines:** height = text block height (± 1U padding above/below first/last element)
- **Horizontal lines:** width ≥ widest text element in its zone (up to 120%, NEVER shorter)
```css
.vline {
position: absolute;
top: var(--content-top); /* align with first text element */
bottom: var(--content-bottom); /* align with last text element */
}
.hline {
width: max(100%, 200px); /* at least as wide as parent text container */
}
```
**Checklist:**
- [ ] Every vertical line’s height matches its adjacent text block span
- [ ] Every horizontal line’s width ≥ widest text element in its zone
- [ ] No decorative line is shorter than the text it accompanies
---
## S3.8 - Vertical Balance (Anti-Top-Heavy Layout)
**Problem:** Content clusters at top of page, bottom 40%+ is dead whitespace.
**Rule:** When total content height < 50% of page height, switch to centered distribution (flexbox `justify-content: center`). Otherwise use anchor grid:
| Content Volume | Title Anchor | Summary Anchor | Meta Anchor |
|---------------|-------------|----------------|-------------|
| **Sparse** (fill < 50%) | Centered mode | Centered mode | Centered mode |
| **Normal** (fill 50-80%) | `H * 0.30` | `H * 0.48` | `H * 0.70` |
| **Dense** (fill > 80%) | `H * 0.20` | `H * 0.40` | `H * 0.65` |
**CJK Title Size Compensation:** CJK characters appear smaller at same pt size. Increase by 15-20%:
- CJK Hero Title: 50-80pt (Latin: 45-65pt)
- CJK Kicker: 11-12pt (Latin: 9pt)
- CJK Summary: 17-20pt (Latin: 16-18pt)
**Checklist:**
- [ ] No cover has >40% dead whitespace at the bottom
- [ ] CJK titles are 15-20% larger than equivalent Latin titles
- [ ] Sparse-content covers use centered distribution
---
## S3.9 - Percentage Positioning Requires Known-Size Container
**Problem:** `top: XX%` resolves against containing block’s height. If height is undefined (all children absolutely positioned), percentages collapse and elements stack.
**Iron rule:** Container with `top: XX%` children MUST have deterministic height (`height: 100%`, `inset: 0`, or `top` + `bottom` pair).
```css
/* ✅ Safest: flat structure with px values */
.cover { position: relative; width: 794px; height: 1123px; }
.kicker { position: absolute; top: 225px; left: 95px; }
.title { position: absolute; top: 292px; left: 95px; }
/* ✅ OK: wrapper with inset:0 gives it known height */
.content-left { position: absolute; inset: 0; width: 55%; }
.title { position: absolute; top: 26%; } /* 26% of 1123px ✓ */
/* ❌ BANNED: wrapper without height, percentage is undefined */
.content-left { position: absolute; left: 12%; top: 0; width: 55%; }
.title { position: absolute; top: 26%; } /* 26% of WHAT? → overlap */
```
---
# PART 4: COVER COLOR RULES
> Cover colors must be consistent with the body color system - they cannot exist independently.
> **The cascade palette CSS output includes `--c-*` aliases** that map directly to cover template variables. Paste the CSS output into cover HTML's `:root` block — template 01 works without any manual color translation.
```
Cover primary = Body theme color
Cover secondary = Primary lightness variant (±20% lightness)
Cover background = Pure white / very light gray / primary at 5-8% opacity
```
### Absolutely Forbidden
- ❌ Dark large-area solid backgrounds (dark blue, dark green, black filling the page)
- ❌ Gradient backgrounds (any `linear-gradient` / `radial-gradient` as large-area fill)
- ❌ High-saturation color schemes
- ❌ Rainbow / multi-color gradients
- ❌ Dense textures or patterns
- ❌ Piling on decorative elements - restraint > clutter
- ❌ More than 2 typefaces on a cover
- ❌ Centered text + gradient/solid background (PowerPoint aesthetic)
### Safe Cover Color Schemes (Reference Only)
> ⚠️ These are **examples for reference**. In normal workflow, run `palette.cascade --title "<title>" --mode minimal --format css` to generate the actual cover colors. Do NOT copy these hex values directly.
> ✅ **Cascade CSS output includes `--c-*` aliases.** The `--format css` output automatically appends cover-compatible aliases (`--c-bg`, `--c-accent`, `--c-text`, `--c-muted`, `--c-mid`, `--c-surface`) mapped from cascade roles. Template 01 can consume the CSS output directly — no manual variable translation needed.
>
> | Cover alias | Cascade source | Tier | Purpose |
> |------------|---------------|------|---------|
> | `--c-bg` | `--page-bg` | XL | Cover background |
> | `--c-accent` | `--accent` | XS | Decorative lines, labels |
> | `--c-text` | `--text-primary` | text | Title text |
> | `--c-muted` | `--text-muted` | text | Subtitle, footer |
> | `--c-mid` | `--header-fill` | M | Structural fills |
> | `--c-surface` | `--card-bg` | L | Card/container bg |
| Name | Primary | Secondary | Background | Use Case |
|------|---------|-----------|------------|----------|
| Ink Stone | `#1a1a2e` | `#4a4a5e` | `#fafafa` | Business, formal |
| Indigo | `#1e3a5f` | `#2d5f8a` | `#f5f8fb` | Technology, reports |
| Warm Chestnut | `#5c3d2e` | `#8a6b5a` | `#faf6f3` | Culture, branding |
| Moss Green | `#2d4a3e` | `#4a7a6a` | `#f5f8f6` | Nature, health |
| Deep Crimson | `#6b2d3e` | `#9a4a5e` | `#faf5f6` | Traditional, elegant |
---
# PART 4.5: ACADEMIC COVER TEMPLATES (Templates 03-04)
> **Academic covers are exempt from PART 4 color rules.** Academic papers, theses, and research reports traditionally use dark backgrounds with light text - this is the established scholarly visual language. Templates 03-04 follow LaTeX title page conventions translated to HTML/CSS.
**All 2 templates share these rules:**
- Page size: `width: 794px; height: 1123px` (A4 at 96dpi)
- Full-bleed dark background (edge-to-edge, no margins)
- Serif font for titles (Playfair Display / Noto Serif SC), sans-serif for metadata
- Generated as HTML → Playwright `page.pdf()` → pypdf merge (same pipeline as Report covers)
- `<link>` tag for Google Fonts (NOT `@import`)
**Content slots (all templates):**
| Slot | Required | Example |
|------|----------|---------|
| `label` | Optional | `RESEARCH PAPER`, `博士论文` |
| `title` | **Required** | Paper title (auto-wrap, max 3 lines) |
| `subtitle` | Optional | Subtitle or abstract excerpt |
| `authors` | **Required** | Author name(s) |
| `institution` | Optional | University / lab / affiliation |
| `keywords` | Optional | Keyword list |
| `footer_left` | Optional | Journal name, DOI |
| `footer_right` | Optional | Date, version |
---
## Template 03: Academic Vertical Anchor - Dark bg + Left vertical line + Left-aligned
**Design intent:** Emulates the classic arXiv/preprint cover. A bold vertical accent line anchors the left edge, all text left-aligned with generous vertical rhythm. Serious, no-frills.
```
┌─────────────────────────────┐
│ ┃ │
│ ┃ LABEL (9pt, accent) │ ← y = H - 3.5cm
│ ┃ │
│ ┃ Title (32pt Bold) │ ← y = H - 6cm, line-height 42pt
│ ┃ Title line 2 │
│ ┃ │
│ ┃ Subtitle (12pt) │ ← y = H - 14cm
│ ┃ │
│ ┃ Authors (12pt, white) │ ← y = H - 18cm
│ ┃ Institution (10pt) │
│ ┃ │
│ ┃ Footer L Footer R │
└─────────────────────────────┘
┃ = vertical accent line at x=1.5cm, 2.5pt width
```
**Best for:** Research papers, technical reports, arXiv preprints
**HTML structure:**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Noto+Serif+SC:wght@400;700;900&family=Inter:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
<style>
@page { size: 794px 1123px; margin: 0; }
:root {
--c-bg: #162032;
--c-accent: #8B7E5A;
--c-text: #FFFFFF;
--c-muted: #8898A8;
--c-footer: #607080;
}
html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Inter', 'Noto Sans SC', sans-serif; }
@media screen {
html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: var(--c-bg); }
body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.3); }
}
.cover { width: 794px; height: 1123px; position: relative; box-sizing: border-box; }
.vline { position: absolute; left: 57px; top: 76px; bottom: 76px; width: 2.5px; background: var(--c-accent); }
.content { position: absolute; left: 83px; right: 76px; top: 0; bottom: 0; }
.label { position: absolute; top: 132px; font-size: 9pt; color: var(--c-accent); letter-spacing: 3px; text-transform: uppercase; font-family: 'Inter', 'Noto Sans SC', sans-serif; }
.title { position: absolute; top: 228px; font-size: 32pt; font-weight: 700; line-height: 1.3; font-family: 'Playfair Display', 'Noto Serif SC', serif; color: var(--c-text); max-width: 580px; }
.subtitle { position: absolute; top: 530px; font-size: 12pt; line-height: 1.5; color: var(--c-muted); max-width: 500px; }
.authors { position: absolute; top: 680px; font-size: 12pt; color: var(--c-text); }
.institution { position: absolute; top: 740px; font-size: 10pt; color: var(--c-muted); line-height: 1.4; }
.footer { position: absolute; bottom: 76px; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 9pt; color: var(--c-footer); }
</style>
</head>
<body>
<div class="cover">
<div class="vline"></div>
<div class="content">
<div class="label"><!-- LABEL --></div>
<div class="title"><!-- TITLE --></div>
<div class="subtitle"><!-- SUBTITLE --></div>
<div class="authors"><!-- AUTHORS --></div>
<div class="institution"><!-- INSTITUTION --></div>
<div class="footer">
<span><!-- FOOTER_LEFT --></span>
<span><!-- FOOTER_RIGHT --></span>
</div>
</div>
</div>
</body>
</html>
```
**Default palette (override via `palette.cascade --intent <intent> --mode dark --format css`):**
| Name | `--c-bg` | `--c-accent` | `--c-muted` | Use case |
|------|----------|------------|----------|----------|
| Deep Sea | `#162032` | `#8B7E5A` | `#8898A8` | General academic |
| Indigo | `#1e3a5f` | `#2d5f8a` | `#7A90A5` | Technical |
| Ink Stone | `#1a1a2e` | `#4a4a5e` | `#8080A0` | Formal occasions |
> ⚠️ These are **fallback defaults** when the palette system is unavailable. In normal workflow, run `palette.cascade` to generate mathematically harmonious colors and inject them into the `:root` variables.
---
## Template 04: Academic Symmetric - Dark bg + Top/bottom lines + Centered
**Design intent:** Emulates the classic IEEE/ACM Transactions title page. Perfect bilateral symmetry, thick horizontal rules frame the content zone. Formal and authoritative.
```
┌─────────────────────────────┐
│ │
│ ══════════════════════ │ ← Top rule y=H-3cm, 2pt
│ │
│ LABEL (centered) │
│ │
│ Title (28-30pt Bold) │
│ │
│ Subtitle │
│ │
│ Authors │
│ Institution │
│ │
│ ══════════════════════ │ ← Bottom rule y=3cm, 2pt
│ Journal · Date │
└─────────────────────────────┘
```
**Best for:** Top journal submissions, IEEE/ACM papers, theses
**HTML structure:**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Noto+Serif+SC:wght@400;700;900&family=Inter:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet">
<style>
@page { size: 794px 1123px; margin: 0; }
:root {
--c-bg: #162032;
--c-accent: #4A90C4;
--c-text: #FFFFFF;
--c-muted: #90A8C0;
}
html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Inter', 'Noto Sans SC', sans-serif; }
@media screen {
html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: var(--c-bg); }
body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.3); }
}
.cover { width: 794px; height: 1123px; position: relative; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; }
.rule-top, .rule-bottom { position: absolute; left: 114px; right: 114px; height: 2px; background: var(--c-accent); }
.rule-top { top: 114px; }
.rule-bottom { bottom: 114px; }
.center-block { position: absolute; top: 0; bottom: 0; left: 114px; right: 114px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
.label { font-size: 9pt; color: var(--c-accent); letter-spacing: 3px; text-transform: uppercase; margin-bottom: 40px; font-family: 'Inter', 'Noto Sans SC', sans-serif; }
.title { font-size: 30pt; font-weight: 700; line-height: 1.3; font-family: 'Playfair Display', 'Noto Serif SC', serif; margin-bottom: 24px; max-width: 500px; }
.subtitle { font-size: 14pt; color: var(--c-muted); margin-bottom: 40px; max-width: 450px; line-height: 1.5; }
.authors { font-size: 12pt; margin-bottom: 12px; }
.institution { font-size: 10pt; color: var(--c-muted); line-height: 1.4; }
.footer { position: absolute; bottom: 57px; left: 114px; right: 114px; text-align: center; font-size: 9pt; color: var(--c-muted); }
</style>
</head>
<body>
<div class="cover">
<div class="rule-top"></div>
<div class="rule-bottom"></div>
<div class="center-block">
<div class="label"><!-- LABEL --></div>
<div class="title"><!-- TITLE --></div>
<div class="subtitle"><!-- SUBTITLE --></div>
<div class="authors"><!-- AUTHORS --></div>
<div class="institution"><!-- INSTITUTION --></div>
</div>
<div class="footer"><!-- FOOTER --></div>
</div>
</body>
</html>
```
**Default palette (override via `palette.cascade --intent <intent> --mode dark --format css`):**
| Name | `--c-bg` | `--c-accent` | `--c-muted` | Use case |
|------|----------|------------|----------|----------|
| Midnight Blue | `#162032` | `#4A90C4` | `#90A8C0` | Math/theoretical |
| Ink Blue | `#0D1B2A` | `#3D5A80` | `#8898A8` | Formal reports |
| Deep Navy | `#0a1628` | `#5B8DB8` | `#7A9AB5` | Engineering |
> ⚠️ These are **fallback defaults**. In normal workflow, run `palette.cascade` to generate colors and inject into `:root`.
---
---
## Template 06: Institutional - White bg + Black border frame + Structured field slots
**Design intent:** The universal institutional cover. White background with a thick black border frame, all content centered, structured field slots with underline placeholders. Matches the style required by most universities worldwide for thesis proposals, dissertations, and formal institutional documents. Also suitable for government reports and official submissions. Zero decorative elements - the formality IS the design.
**⚠️ This template is exempt from PART 4 Academic Cover Color Rules (dark backgrounds).** It uses a white/light background by design, aligning with institutional formatting requirements.
```
┌─────────────────────────────────┐
│ ┌─────────────────────────────┐ │
│ │ │ │
│ │ INSTITUTION NAME │ │ ← y = 12%, serif 28-34pt Bold
│ │ (校名/机构名) │ │
│ │ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━ │ │ ← thick divider (2pt)
│ │ │ │
│ │ DOCUMENT TYPE │ │ ← y = 30%, 20-24pt
│ │ (开题报告/毕业论文/申报书) │ │
│ │ │ │
│ │ TITLE │ │ ← y = 40%, serif 26-30pt Bold
│ │ (论文题目) │ │ max 3 lines, centered
│ │ │ │
│ │ Field: _______________ │ │ ← y = 58-78%, structured fields
│ │ Field: _______________ │ │ left-label + underline value
│ │ Field: _______________ │ │ e.g. 姓名、学号、导师、院系、日期
│ │ Field: _______________ │ │
│ │ Field: _______________ │ │
│ │ │ │
│ │ DATE │ │ ← y = 88%, centered, 14pt
│ │ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
││ = 2.5pt black border, inset 5% from page edge
```
**Best for:** Thesis proposals (开题报告), dissertations, institutional reports, government documents, any formal submission with structured metadata fields
**Content slots:**
| Slot | Required | Example |
|------|----------|---------|
| `institution` | **Required** | "北京大学", "Massachusetts Institute of Technology" |
| `doc_type` | Optional | "开题报告", "Thesis Proposal", "毕业设计" |
| `title` | **Required** | Paper/document title (auto-wrap, max 3 lines) |
| `fields` | Optional | Array of `{label, value}` pairs. Common: 姓名/Name, 学号/ID, 导师/Advisor, 院系/Department, 专业/Major |
| `date` | Optional | "2026年4月", "April 2026" |
**HTML structure:**
```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&family=Playfair+Display:wght@400;700;900&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
<style>
@page { size: 794px 1123px; margin: 0; }
:root {
--c-bg: #ffffff;
--c-text: #1a1a1a;
--c-accent: #1a1a1a;
--c-muted: #4a4a4a;
--c-line: #333333;
}
html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Noto Sans SC', 'Inter', sans-serif; }
@media screen {
html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: #e8e8e8; }
body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.15); }
}
.cover {
width: 794px; height: 1123px; position: relative; box-sizing: border-box;
}
/* Black border frame - inset 5% from page edge */
.border-frame {
position: absolute;
top: 56px; left: 40px; right: 40px; bottom: 56px;
border: 2.5px solid var(--c-accent);
pointer-events: none;
}
/* Content area inside frame */
.content {
position: absolute;
top: 56px; left: 40px; right: 40px; bottom: 56px;
display: flex; flex-direction: column; align-items: center;
padding: 60px 50px;
box-sizing: border-box;
}
.institution {
font-size: 30pt; font-weight: 700; letter-spacing: 6px;
font-family: 'Noto Serif SC', 'Playfair Display', serif;
text-align: center; margin-bottom: 30px;
max-width: 580px;
}
.thick-divider {
width: 70%; height: 2px; background: var(--c-accent);
margin-bottom: 40px;
}
.doc-type {
font-size: 22pt; font-weight: 400; letter-spacing: 4px;
text-align: center; margin-bottom: 50px;
color: var(--c-text);
}
.title {
font-size: 26pt; font-weight: 700; line-height: 1.4;
font-family: 'Noto Serif SC', 'Playfair Display', serif;
text-align: center; margin-bottom: 60px;
max-width: 520px;
}
.fields-block {
width: 400px; margin-bottom: auto;
}
.field-row {
display: flex; align-items: baseline;
margin-bottom: 28px; font-size: 14pt;
}
.field-label {
white-space: nowrap; margin-right: 12px;
color: var(--c-text); font-weight: 400;
letter-spacing: 2px;
}
.field-value {
flex: 1; text-align: center;
border-bottom: 1px solid var(--c-line);
padding-bottom: 4px; min-height: 24px;
font-family: 'Noto Sans SC', 'Inter', sans-serif;
}
.date-block {
font-size: 14pt; color: var(--c-muted);
text-align: center; letter-spacing: 2px;
margin-top: auto; padding-top: 30px;
}
</style>
</head>
<body>
<div class="cover">
<div class="border-frame"></div>
<div class="content">
<div class="institution"><!-- INSTITUTION --></div>
<div class="thick-divider"></div>
<div class="doc-type"><!-- DOC_TYPE --></div>
<div class="title"><!-- TITLE --></div>
<div class="fields-block">
<div class="field-row">
<span class="field-label"><!-- LABEL_1 --></span>
<span class="field-value"><!-- VALUE_1 --></span>
</div>
<div class="field-row">
<span class="field-label"><!-- LABEL_2 --></span>
<span class="field-value"><!-- VALUE_2 --></span>
</div>
<div class="field-row">
<span class="field-label"><!-- LABEL_3 --></span>
<span class="field-value"><!-- VALUE_3 --></span>
</div>
<div class="field-row">
<span class="field-label"><!-- LABEL_4 --></span>
<span class="field-value"><!-- VALUE_4 --></span>
</div>
<div class="field-row">
<span class="field-label"><!-- LABEL_5 --></span>
<span class="field-value"><!-- VALUE_5 --></span>
</div>
</div>
<div class="date-block"><!-- DATE --></div>
</div>
</div>
</body>
</html>
```
**Layout rules:**
1. **Border frame**: 2.5pt solid black, inset ~5% from all page edges (40px left/right, 56px top/bottom on A4 at 96dpi). This is the defining visual element.
2. **Institution name**: Centered, serif, 28-34pt Bold, letter-spacing 4-6px. For CJK names, use wider letter-spacing (6px). For Latin names, use standard (3px).
3. **Thick divider**: 2pt solid line, 70% width, separates institution from content below.
4. **Document type**: 20-24pt, lighter weight than institution name, letter-spacing 3-4px. This slot differentiates document categories (e.g. "开题报告" / "Thesis Proposal" / "毕业论文" / "Graduation Design").
5. **Title**: Serif, 26-30pt Bold, max 3 lines, centered. Auto-wrap at 520px width.
6. **Structured fields**: Left-aligned label + centered underline value. Label width fixed by longest label in the set. 3-7 field rows supported. Common fields: Name/姓名, Student ID/学号, Advisor/导师, Department/院系, Major/专业.
7. **Date**: Centered at bottom, 14pt, letter-spacing 2px.
**Field auto-detection:**
When the user provides structured metadata (name, student ID, advisor, etc.), auto-populate the fields block. When no fields are provided, omit the fields-block entirely and let the title expand vertically into the freed space.
**Variant 06B - Double border:**
For extra formality (government documents, official submissions), replace the single border with a double border (outer 2.5pt + inner 1pt, 6px gap):
```css
.border-frame {
border: 2.5px solid var(--c-accent);
outline: 1px solid var(--c-accent);
outline-offset: 6px;
}
```
**This template has NO background decoration layer, NO watermarks, NO gradients.** The black frame + white space IS the design.
---
## Template 07: Crystal Blue - Deep Blue Night Sky with Luminous Accents
**Design intent:** Crystalline, translucent deep blue with glowing accents. Dark cover transitions to light-blue body. The entire document lives in one blue hue family (~215°). Subtle radial glow hints create glass-like depth without being flashy.
### Color System
**Cover (dark):**
| Role | Hex | Description |
|------|-----|-------------|
| Background | `#0a1628` | Deep blue night sky |
| Accent line/label | `#4da8da` | Luminous bright blue |
| Title text | `#e8f0f8` | Cool white with blue tint |
| Muted text | `#7a9bb8` | Blue-gray |
| Glow/sidebar | `#2d7ab3` | Mid-blue (gradient source) |
**Body (light) — same hue family:**
| Role | Cascade Tier | Hex | Usage |
|------|-------------|-----|-------|
| Page background | XL | `#f5f8fc` | Ultra-light blue-white |
| Section background | XL | `#edf2f9` | Light blue-gray |
| Card background | L | `#e4ecf5` | Soft blue card |
| Table stripe | L | `#eef3fa` | Subtle blue row |
| Table header | M | `#1a4a7a` | Deep blue (bridges to cover) |
| Border | S | `#c0d0e2` | Blue-gray lines |
| Accent | XS | `#2d7ab3` | = cover glow color |
| Text primary | text | `#142840` | Deep blue-black (not pure black) |
| Text muted | text | `#5a7a96` | Blue-gray secondary |
### Layer 2 - Structure
- **Rectangular frame:** A single continuous rectangular border, inset 60px from all edges (top/bottom = 80px). Stroke = **2px**, color = `#4da8da` (accent). This forms a clean luminous blue frame around the entire cover content area.
- Top edge: `Y = 80px`, from `X = 60px` to `X = W - 60px`
- Bottom edge: `Y = H - 80px`, from `X = 60px` to `X = W - 60px`
- Left edge: `X = 60px`, from `Y = 80px` to `Y = H - 80px`
- Right edge: `X = W - 60px`, from `Y = 80px` to `Y = H - 80px`
- **Background glow (optional):** Two subtle `radial-gradient` circles (6-8% opacity) positioned at upper-left and lower-right. Creates depth without visible shapes.
### Layer 3 - Content
| Element | Position | Constraints |
|---------|----------|-------------|
| Kicker | `X = 90px`, `Y = 0.12*H` | 11pt, accent color (#4da8da), letter-spacing 5px, uppercase, font-weight 300 |
| Hero Title | `X = 90px`, `Y = 0.21*H` | 50pt (CJK 55pt), weight 900, Noto Serif SC, cool white (#e8f0f8), subtle text-shadow with blue glow |
| Summary | `X = 90px`, `Y = 0.45*H` | 15pt, line-height 1.7, muted (#7a9bb8), max-width 520px |
| Organization | `X = 90px`, `Y = 0.64*H` | 18pt, cool white |
| Date | `X = 90px`, `Y = H - 80px` | 10pt, muted, letter-spacing 3px |
### Recommended Palette Generation
```bash
# For documents using this template, the palette is fixed (not auto-generated).
# Copy the body palette values directly from the table above.
# Cover colors are hardcoded in the template HTML.
```
### Best For
Technology reports, data analysis, AI/ML research, fintech, SaaS product reports, anything that benefits from a modern, polished, slightly premium blue aesthetic.
---
### Academic Template Selection Guide
| Scenario | Template | Rationale |
|----------|----------|-----------|
| arXiv preprint, technical report | **03** (Vertical Anchor) | Left-aligned, data-dense feel |
| IEEE/ACM paper, English thesis | **04** (Symmetric) | Classic bilateral symmetry |
| Chinese thesis, journal with keywords | **04** (Symmetric) | Classic symmetry + keyword support via subtitle/summary slots |
| **Thesis proposal, institutional cover, government doc** | **06** (Institutional) | **White bg, black border frame, structured field slots** |
| Light/formal academic (white bg) | **01** (HUD) | Use standard cover system |
Covers support a **background decoration layer** rendered behind all foreground content (Layer 1). This layer adds subtle depth through supergraphics, typographic watermarks, and blueprint hairlines.
See → `typesetting/cover-backgrounds.md` - complete specification with modules, recipes, and constraint matrix.
**Quick reference:**
- **Recipe A (Minimalist Modern)**: Deep-space arc only - safest, max whitespace
- **Recipe B (Engineering Academic)**: Coordinate cross + vertical spine text - precision, engineering feel
- **Recipe C (Stable & Authoritative)**: Angle slash + bottom bleed text - heavy, authoritative
**Background layer is OPTIONAL.** Not every cover needs one. Template 01 already defines its own Layer 1 background - use the recipes only when a template's built-in background is insufficient.
+527
View File
@@ -0,0 +1,527 @@
# Fill Engine — Adaptive Anti-Void Layout Engine V2.0
> Solves the **"text too small to read"** and **"large page voids"** problems caused by varying input content length in automated PDF generation.
> Before rendering any page, it must pass through the following **four elastic filtering calculations**.
>
> **Positioning**: This is the mirror counterpart of `overflow.md` (anti-overflow) — overflow handles "too much content", fill-engine handles "too little content".
>
> Related:
> - `overflow.md` — Anti-overflow layout system (degradation strategy for excessive content)
> - `pagination.md` — Pagination & cross-page integrity control
> - `cover.md` — Cover layout engine (covers not affected by Fill Engine; they have their own layout system)
---
## ⚠️ Scope of Application
- ✅ **Body pages** (body pages of Report / Academic / Creative routes)
- ✅ **All three rendering routes** (ReportLab / LaTeX / Playwright-HTML)
- ❌ **Not applicable to covers** (covers are independently controlled by `cover.md`)
- ❌ **Not applicable to TOC pages** (TOC has a fixed format)
---
## Safety Net 1: Readability Red Line (Font Size Hard Floor)
**Principle: Never rely on shrinking font size to fit the layout. Font sizes have hard-coded minimums that cannot be breached.**
### Absolute Font Size Floor
| Element | Single-column | Double-column | Notes |
|------|---------|---------|------|
| **Body Text** | **≥ 14pt** | **≥ 12pt** | Below this → considered unreadable, must trigger page break |
| **Default base size** | 15pt (CJK) / 14pt (Latin) | 13pt (CJK) / 12pt (Latin) | Starting value, not ceiling |
### Heading Scale Hard Floor
| Level | Min Size | Recommended |
|------|---------|---------|
| **H1** (primary heading / page title) | **≥ 32pt** | 36–42pt |
| **H2** (secondary heading) | **≥ 24pt** | 26–30pt |
| **H3** (tertiary heading) | **≥ 18pt** | 20–22pt |
### Coordination with overflow.md
`overflow.md` §5's font-size degradation staircase (`fit_text_with_degradation`) **must not breach the above floor**:
```python
# overflow.md §5's min_size parameter must be >= Fill Engine red line
def fit_text_with_degradation(text, font_name, base_size, max_width,
min_size=14): # ← Single-column floor 14pt, not 7pt
"""When overflow needs to shrink font size, it cannot go below the readability red line."""
for size in range(base_size, min_size - 1, -1):
if stringWidth(text, font_name, size) <= max_width:
return size
return min_size # Hit the floor → trigger page break, stop shrinking
```
> **Core idea: If content doesn't fit → page break, not shrinking text to ant-size.**
---
## Safety Net 2: Page Fill Ratio & Paragraph Inflation (Fill Ratio Engine)
### Virtual Rendering
Before actually rendering each page, the system **must first calculate** how much height the content would occupy under default font sizes and spacing.
```python
def calculate_fill_ratio(content_blocks, available_height, default_styles):
"""
Virtual rendering: calculate total height of current page content under default styles.
Returns fill ratio = total content height / available page height.
"""
total_height = 0
for block in content_blocks:
block_height = measure_block_height(block, default_styles)
total_height += block_height
fill_ratio = total_height / available_height
return fill_ratio
```
### Elastic Inflation Trigger Conditions
| Fill Ratio | Status | Action |
|--------|------|------|
| **≥ 80%** | ✅ Full | No adjustment, render normally |
| **65%–80%** | ⚠️ Slightly empty | Light inflation (line-height + paragraph spacing only) |
| **40%–65%** | 🔶 Noticeably empty | Full inflation (line-height + spacing + slight font increase + component inflation) |
| **< 40%** | Extremely empty | Full inflation + Y-axis golden ratio anchoring (Safety Net 4) |
### Inflation Parameters (Triggered when fill ratio < 65%)
#### 2a. Line-Height Inflation
```python
def inflate_line_height(base_line_height, fill_ratio):
"""
Lower fill ratio means more line-height stretch.
base_line_height: default line-height (e.g. 1.4)
Returns inflated line-height, capped at 2.2.
"""
if fill_ratio >= 0.65:
return base_line_height # No inflation
# Linear interpolation: as fill_ratio goes from 0.65→0.30, line-height goes from base→2.2
inflation = (0.65 - fill_ratio) / (0.65 - 0.30) # 0.0 ~ 1.0
inflation = min(inflation, 1.0)
target = base_line_height + (2.2 - base_line_height) * inflation
return round(target, 2)
```
| Fill Ratio | Base line-height 1.4 → After inflation |
|--------|---------------------|
| 65% | 1.40 (unchanged) |
| 55% | 1.63 |
| 45% | 1.86 |
| 35% | 2.09 |
| ≤30% | 2.20 (cap) |
#### 2b. Paragraph Spacing Compensation (Margin-Bottom Injection)
```python
def inject_paragraph_spacing(remaining_height, paragraph_count, heading_count):
"""
Distribute 30%-50% of remaining whitespace evenly between paragraphs.
remaining_height: Available_H - content height after inflation
"""
if remaining_height <= 0:
return 0
injection_pool = remaining_height * 0.4 # Take 40%
gap_count = paragraph_count + heading_count - 1 # Number of gaps
if gap_count <= 0:
return 0
per_gap = injection_pool / gap_count
return round(per_gap, 1)
```
**Injection positions (by priority):**
1. Between headings and body text (below H1/H2/H3)
2. Between natural paragraphs
3. Between body text and charts/tables
#### 2c. Font Scaling
```python
def scale_font_size(base_size, fill_ratio):
"""
When fill ratio < 65%, allow font size to float up by 1-2pt.
Never exceed +2pt, otherwise loses professional feel.
"""
if fill_ratio >= 0.65:
return base_size
if fill_ratio >= 0.50:
return base_size + 1
return base_size + 2 # Max +2pt
```
> **Constraint: Inflated font size ≤ base_size + 2pt. 15pt body can become at most 17pt, no larger.**
---
## Safety Net 3: Component-Level Elastic Fill (Component Inflation)
**Trigger: Same as Safety Net 2, active when fill ratio < 65%.**
### 3a. Table Auto-Height Expansion (Table Padding Inflation)
```python
def inflate_table_padding(base_padding, fill_ratio):
"""
Lower fill ratio means larger table cell padding.
base_padding: default cell padding (e.g. 6pt)
"""
if fill_ratio >= 0.65:
return base_padding
# Add 10-20pt
extra = int((0.65 - fill_ratio) / 0.25 * 20)
extra = max(10, min(extra, 20))
return base_padding + extra
```
**Effect:** Originally flat compact data table → tall spacious data display board.
### 3b. Blockquote Exaggeration (Blockquote Scaling)
When encountering a blockquote and the page has voids:
```python
def scale_blockquote(base_font_size, fill_ratio):
"""
Blockquote font enlarged, italicized, massive whitespace above and below.
"""
if fill_ratio >= 0.65:
return {
"font_size": base_font_size,
"font_style": "normal",
"margin_top": 12,
"margin_bottom": 12,
"border_left_width": 3,
}
return {
"font_size": int(base_font_size * 1.5), # Scale up 1.5x
"font_style": "italic",
"margin_top": 40, # Large whitespace above
"margin_bottom": 40, # Large whitespace below
"border_left_width": 6, # Thicken blockquote left border
}
```
### 3c. List Item Spacing Expansion
```python
def inflate_list_spacing(base_spacing, fill_ratio):
"""
List item spacing expanded to 1.5x normal paragraph spacing.
"""
if fill_ratio >= 0.65:
return base_spacing
return int(base_spacing * 1.5)
```
---
## Safety Net 4: Y-Axis Golden Ratio Anchoring (Ultimate Measure for Extreme Voids)
**Trigger: After Safety Net 2 + 3 inflation, fill ratio still < 40%.**
**Core principle: Absolutely forbidden to pin content to the very top, leaving the bottom half as dead whitespace.**
### Execution Logic
```python
def anchor_content_vertically(content_bbox_height, available_height, fill_ratio):
"""
Pack all current page content as a BBox, re-align vertically within available height.
Returns content_top_y: Y coordinate offset for content top.
"""
if fill_ratio >= 0.40:
return 0 # No anchoring needed, normal flow from page top
remaining = available_height - content_bbox_height
# Option A: Golden ratio offset-up (recommended)
golden_offset = remaining * 0.382 # Top 38.2%, bottom 61.8%
# Option B: Absolute vertical center (alternative)
# center_offset = remaining / 2
return golden_offset
```
### Option Selection
| Option | Formula | Visual Effect | Applicable Scenario |
|------|------|---------|---------|
| **A. Golden ratio offset-up** (default) | `offset = remaining * 0.382` | Slightly less whitespace above, more below, visually stable | Most scenarios |
| **B. Absolute center** | `offset = remaining / 2` | Perfectly symmetrical | Minimal pages with single element |
### Effect Illustration
```
┌─────────────────────────┐ ┌─────────────────────────┐
│ ← Content starts here │ │ │
│ Section Title │ │ ← 38.2% elegant space │
│ Body text... │ │ │
│ │ │ Section Title │
│ │ │ Body text... │
│ │ │ │
│ │ │ │
│ ← Huge dead whitespace! │ │ ← 61.8% bottom space │
│ │ │ │
│ │ │ │
└─────────────────────────┘ └─────────────────────────┘
❌ No anchoring ✅ Golden ratio anchoring
```
---
## Three-Route Implementation Guide
### ReportLab Route (Report Pipeline)
```python
from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph
from reportlab.lib.units import pt
def build_page_with_fill_engine(story_blocks, page_width, page_height, margins):
"""
Fill Engine main entry — ReportLab route.
Call before doc.build().
"""
available_h = page_height - margins['top'] - margins['bottom']
available_w = page_width - margins['left'] - margins['right']
# --- Safety Net 1: Check font size floor ---
enforce_font_floor(story_blocks, min_body=14, min_h1=32, min_h2=24, min_h3=18)
# --- Safety Net 2: Virtual render + inflation ---
fill_ratio = calculate_fill_ratio(story_blocks, available_h, default_styles)
if fill_ratio < 0.65:
# 2a. Line-height inflation
new_line_height = inflate_line_height(1.4, fill_ratio)
apply_line_height(story_blocks, new_line_height)
# 2b. Paragraph spacing injection
remaining = available_h - measure_total_height(story_blocks)
extra_gap = inject_paragraph_spacing(remaining, count_paragraphs(story_blocks),
count_headings(story_blocks))
inject_spacers(story_blocks, extra_gap)
# 2c. Font scaling
font_bump = scale_font_size(15, fill_ratio) - 15
if font_bump > 0:
bump_font_sizes(story_blocks, font_bump)
# --- Safety Net 3: Component inflation ---
if fill_ratio < 0.65:
inflate_tables(story_blocks, fill_ratio)
inflate_blockquotes(story_blocks, fill_ratio)
inflate_lists(story_blocks, fill_ratio)
# --- Safety Net 4: Y-axis anchoring ---
recalc_ratio = calculate_fill_ratio(story_blocks, available_h, inflated_styles)
if recalc_ratio < 0.40:
content_height = measure_total_height(story_blocks)
top_offset = anchor_content_vertically(content_height, available_h, recalc_ratio)
story_blocks.insert(0, Spacer(1, top_offset))
return story_blocks
```
### LaTeX Route (Academic Pipeline)
```latex
% Safety Net 1: Font size floor — define in preamble
\newcommand{\bodysize}{\fontsize{14pt}{20pt}\selectfont} % 14pt floor
\renewcommand{\Large}{\fontsize{32pt}{38pt}\selectfont} % H1 ≥ 32pt
\renewcommand{\large}{\fontsize{24pt}{30pt}\selectfont} % H2 ≥ 24pt
% Safety Net 4: Vertical centering (for extreme voids)
\newcommand{\goldenpage}[1]{%
\null\vfill % Top elastic space (less)
#1 % Content
\vfill\vfill % Bottom elastic space (more, ~2:1 ratio)
}
% Usage (when content is minimal):
% \goldenpage{
% \section{Summary}
% Short content here...
% }
```
### Playwright/HTML Route (Creative Pipeline)
```css
/* Safety Net 1: Font size red line */
:root {
--body-min-font: 14px;
--h1-min-font: 32px;
--h2-min-font: 24px;
--h3-min-font: 18px;
}
body {
font-size: max(var(--body-font, 15px), var(--body-min-font));
}
h1 { font-size: max(var(--h1-font, 36px), var(--h1-min-font)); }
h2 { font-size: max(var(--h2-font, 28px), var(--h2-min-font)); }
h3 { font-size: max(var(--h3-font, 22px), var(--h3-min-font)); }
/* Safety Net 2-3: Dynamically injected after JS virtual render */
/* Before Playwright screenshot, run Fill Engine JS via page.evaluate() */
```
```javascript
// Playwright page.evaluate() — Fill Engine
function runFillEngine(pageElement) {
const pageH = pageElement.clientHeight;
const contentH = pageElement.scrollHeight;
const fillRatio = contentH / pageH;
if (fillRatio >= 0.65) return; // No inflation needed
const root = pageElement.style;
// 2a. Line-height inflation
const inflation = Math.min((0.65 - fillRatio) / 0.35, 1.0);
const newLH = 1.4 + (2.2 - 1.4) * inflation;
root.setProperty('--body-line-height', newLH.toFixed(2));
pageElement.querySelectorAll('p, li').forEach(el => {
el.style.lineHeight = newLH.toFixed(2);
});
// 2c. Font scaling
if (fillRatio < 0.50) {
pageElement.querySelectorAll('p, li').forEach(el => {
const size = parseFloat(getComputedStyle(el).fontSize);
el.style.fontSize = Math.min(size + 2, size + 2) + 'px'; // +2pt max
});
} else if (fillRatio < 0.65) {
pageElement.querySelectorAll('p, li').forEach(el => {
const size = parseFloat(getComputedStyle(el).fontSize);
el.style.fontSize = (size + 1) + 'px'; // +1pt
});
}
// 3a. Table height expansion
pageElement.querySelectorAll('td, th').forEach(cell => {
const extra = Math.min(20, Math.round((0.65 - fillRatio) / 0.25 * 20));
cell.style.paddingTop = (6 + extra) + 'px';
cell.style.paddingBottom = (6 + extra) + 'px';
});
// 3b. Blockquote exaggeration
pageElement.querySelectorAll('blockquote').forEach(bq => {
const size = parseFloat(getComputedStyle(bq).fontSize);
bq.style.fontSize = (size * 1.5) + 'px';
bq.style.fontStyle = 'italic';
bq.style.marginTop = '40px';
bq.style.marginBottom = '40px';
});
// Safety Net 4: Y-axis anchoring
const newContentH = pageElement.scrollHeight;
const newRatio = newContentH / pageH;
if (newRatio < 0.40) {
const remaining = pageH - newContentH;
const offset = remaining * 0.382;
pageElement.style.paddingTop = offset + 'px';
}
}
```
---
## Execution Order Summary
```
Input content arrives
│
▼
┌─────────────────────────────────────────┐
│ Safety Net 1: Readability red line check │
│ → Font size ≥ floor? YES → Continue │
│ → Font size < floor? → Force raise to floor │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Virtual render: Calculate Fill Ratio │
│ → ≥ 80%? → Render normally, exit │
│ → 65%-80%? → Light inflation (2a+2b only) │
│ → < 65%? → Enter Safety Net 2+3 full inflation │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Safety Net 2: Paragraph inflation │
│ 2a. Line-height inflation (→ max 2.2) │
│ 2b. Paragraph spacing injection (30%-50% of remaining space) │
│ 2c. Font scaling (+1~2pt, max) │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Safety Net 3: Component inflation │
│ 3a. Table Padding increase (+10~20pt) │
│ 3b. Blockquote scale 1.5x + 40pt whitespace above/below │
│ 3c. List spacing × 1.5 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Recalculate Fill Ratio │
│ → ≥ 40%? → Render normally, exit │
│ → < 40%? → Safety Net 4: Y-axis golden ratio anchoring │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Safety Net 4: Shift content down │
│ top_offset = remaining * 0.382 │
│ → Elegant space above, slightly more below │
│ → No longer "underfilled" but "intentional whitespace" │
└─────────────────────────────────────────┘
│
▼
Actual render output
```
---
## Coordination with Other Specifications
| Specification | Relationship | Coordination Rule |
|------|------|---------|
| `overflow.md` | Complementary (overflow vs void) | overflow's font degradation must not breach Fill Engine's red line |
| `pagination.md` | Complementary (pagination vs fill) | pagination's "last page ≥ 40%" aligns with Fill Engine's Fill Ratio concept |
| `cover.md` | Independent | Covers have their own layout system, not affected by Fill Engine |
| `typography.md` | Infrastructure | Fill Engine makes elastic adjustments on top of typography-defined fonts/line-heights |
---
## Checklist (Check Before Every Body Page Render)
```
□ Body font size ≥ 14pt (single-column) / 12pt (double-column)?
□ H1 ≥ 32pt、H2 ≥ 24pt、H3 ≥ 18pt?
□ Virtual render calculated Fill Ratio?
□ Inflation triggered when Fill Ratio < 65%?
□ Line-height inflation not exceeding 2.2?
□ Font scaling not exceeding +2pt?
□ Table Padding increment within 10-20pt range?
□ Blockquote scale factor = 1.5x, top/bottom whitespace = 40pt?
□ Y-axis anchoring triggered when Fill Ratio < 40%?
□ Cover page not affected by Fill Engine?
```
+142
View File
@@ -0,0 +1,142 @@
# Geometric Anchors
> Create sophisticated visual anchors from the simplest geometric shapes.
---
## What Are Visual Anchors
Visual anchors are **non-functional decorative elements** on a page, used to:
- Break the flatness of text-only / data-only layouts
- Establish a visual center of gravity on the page
- Convey abstract qualities and design intent
- Fill large whitespace areas without adding information noise
**Key principle: Anchors don't need to "look like" anything concrete. The more abstract, the more refined.**
---
## Basic Shape Vocabulary
| Shape | SVG | Mood / Character |
|-------|-----|-----------------|
| Circle | `<circle>` | Wholeness, inclusivity, softness |
| Semicircle | `<path d="M0,50 A50,50 0 0,1 100,50">` | Rising, gradual, metaphorical |
| Triangle | `<polygon>` | Direction, sharpness, modern |
| Rectangle | `<rect>` | Stability, order, architectural |
| Line | `<line>` | Connection, guidance, minimalism |
| Arc | `<path>` + Bézier | Flow, elegance, organic |
---
## Composition Patterns
### Pattern 1: Offset Stacking
Multiple identical shapes, slightly offset, rotated, with decreasing opacity.
```svg
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<!-- Three offset circles -->
<circle cx="50" cy="50" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.15"/>
<circle cx="60" cy="55" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.25"/>
<circle cx="70" cy="60" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.4"/>
</svg>
```
**Key points**: Same shape, 3 layers, opacity 0.15 → 0.25 → 0.4, offset 10-15px
### Pattern 2: Scale Contrast
One large shape + a few small shapes as accents.
```svg
<svg width="150" height="150" viewBox="0 0 150 150" fill="none">
<circle cx="60" cy="60" r="50" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
<circle cx="115" cy="30" r="8" fill="currentColor" opacity="0.6"/>
<circle cx="105" cy="50" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="125" cy="45" r="2" fill="currentColor" opacity="0.2"/>
</svg>
```
**Key points**: Large circle stroke-only (hollow), small circles filled (solid), creating solid-void contrast
### Pattern 3: Grid Intersection
Lines + dots forming nodes at intersections.
```svg
<svg width="100" height="100" viewBox="0 0 100 100" fill="none">
<line x1="20" y1="0" x2="20" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/>
<line x1="50" y1="0" x2="50" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/>
<line x1="80" y1="0" x2="80" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/>
<line x1="0" y1="30" x2="100" y2="30" stroke="currentColor" stroke-width="0.3" opacity="0.1"/>
<line x1="0" y1="70" x2="100" y2="70" stroke="currentColor" stroke-width="0.3" opacity="0.1"/>
<!-- Intersection points -->
<circle cx="50" cy="30" r="3" fill="currentColor" opacity="0.5"/>
<circle cx="20" cy="70" r="2" fill="currentColor" opacity="0.3"/>
<circle cx="80" cy="70" r="4" fill="currentColor" opacity="0.15"/>
</svg>
```
### Pattern 4: Arc Flow
Bézier curves + endpoint circles, expressing organic flow.
```svg
<svg width="200" height="100" viewBox="0 0 200 100" fill="none">
<path d="M10,80 C50,10 150,10 190,80" stroke="currentColor" stroke-width="0.6" opacity="0.25"/>
<path d="M10,85 C60,20 140,20 190,85" stroke="currentColor" stroke-width="0.4" opacity="0.15"/>
<circle cx="10" cy="80" r="2.5" fill="currentColor" opacity="0.4"/>
<circle cx="190" cy="80" r="2.5" fill="currentColor" opacity="0.4"/>
</svg>
```
### Pattern 5: Geometric Collage
Intentional combination of different shapes, like an architectural plan.
```svg
<svg width="120" height="120" viewBox="0 0 120 120" fill="none">
<!-- Rectangular frame -->
<rect x="10" y="10" width="60" height="60" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
<!-- Circle breaking the straight lines -->
<circle cx="70" cy="70" r="30" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
<!-- Diagonal line cutting through -->
<line x1="10" y1="10" x2="100" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.15"/>
<!-- Small solid triangle as focal point -->
<polygon points="85,20 95,40 75,40" fill="currentColor" opacity="0.4"/>
</svg>
```
---
## Placement Guide
| Context | Recommended Position | Recommended Size | Pattern |
|---------|---------------------|-----------------|---------|
| Cover | Top-right / bottom-left offset | 120-200px | Offset Stacking, Geometric Collage |
| Chapter divider | Page center | 80-120px | Scale Contrast, Arc Flow |
| Header / footer decoration | Corners | 30-50px | Small offset, single circle + line |
| Whitespace fill | Alongside content | 60-100px | Grid Intersection |
---
## Color Rules
- Anchor color = document primary color from `palette.cascade` output (`--c-accent` or `--c-text`)
- **Use only one color**, layer via opacity (0.1 → 0.5)
- Strokes over fills (solid elements ≤ 30% of total shapes)
- stroke-width range: 0.3-0.8px (ultra-thin lines = refined look)
- **⚠️ All SVG examples below use `currentColor` as placeholder.** When generating actual SVG, replace with the document’s primary color from the palette system. NEVER copy `currentColor` literally into production SVG — substitute the actual hex value.
---
## Forbidden
- ❌ Figurative icons (flowers, stars, arrows, or other concrete shapes)
- ❌ Mixing multiple colors
- ❌ Over-complexity (more than 8 shape elements)
- ❌ Symmetric / centered placement (offset creates tension)
- ❌ Thick lines (> 1.5px looks heavy)
- ❌ Shadows / glow effects
+630
View File
@@ -0,0 +1,630 @@
# Overflow Prevention — Anti-Overflow Layout System
> PDF is static: no scrollbars, no reflow, no viewport adaptation. Every element must fit within its container BEFORE rendering. This document defines the architectural approach to guarantee zero overflow in any generated PDF.
>
> **This is the "too much content" side.** For the mirror problem ("too little content" / empty pages), see `typesetting/fill-engine.md` — the anti-void adaptive engine.
>
> Related:
> - `fill-engine.md` — Anti-void engine (font floor, fill ratio, paragraph inflation, Y-axis anchoring)
> - `pagination.md` — Pagination & cross-page integrity
---
## Core Philosophy
**"Measure first, draw second."**
Never use `draw_text(x, y)` or `draw_image(x, y)` directly. All content must pass through a constraint system that pre-calculates sizes and enforces boundaries. Think CSS Box Model, but for a fixed canvas.
---
## 1. Bounding Box System (Horizontal Overflow Prevention)
Every element entering a page is a **Block** with a maximum available width (`Max_Width`).
### Calculating Max_Width
```python
# Single-column layout
max_width = page_width - left_margin - right_margin
# Dual-column layout
col_gap = 12 # points
max_width = (page_width - left_margin - right_margin - col_gap) / 2
# Nested containers (e.g., table cell)
cell_max_width = col_width - cell_padding_left - cell_padding_right
```
### Absolute Rule
> **No Block's rendered width may exceed its parent's `Max_Width`. Period.**
If a Block's calculated width > Max_Width, apply fallback strategies (see §5).
---
## 1.5 Page Content Centering (Horizontal Centering Iron Rule)
> **Symptom:** Cover or body content is shifted left on the page, with noticeably more whitespace on the right than left.
**Root cause:** Asymmetric left/right margins, or cover uses single-side anchors without considering right-side balance.
**Iron rule:**
1. **Left/right margins must be symmetric:** `left_margin == right_margin`. No asymmetric margins allowed.
2. **Cover:** For left-aligned templates (e.g., Template 01/02), the text starting point should be within `0.10*W ~ 0.15*W`, and the right margin should be between `0.05*W ~ 0.15*W`. Center-aligned templates (Template 04/05/06) must be absolutely centered.
3. **Body:** ReportLab's `Frame` / `SimpleDocTemplate` must have `leftMargin == rightMargin`. LaTeX's `\geometry{left=X, right=X}` must be symmetric. HTML must use `margin: 0 auto` or `padding-left == padding-right`.
```python
# ReportLab: Force symmetric margins
from reportlab.lib.units import inch
MARGIN = 1 * inch # Left and right must use same variable
doc = SimpleDocTemplate(
"output.pdf",
leftMargin=MARGIN,
rightMargin=MARGIN, # ← Must match leftMargin
topMargin=MARGIN,
bottomMargin=MARGIN,
)
# ❌ WRONG: leftMargin=72, rightMargin=36 → Content shifts left
```
```latex
% LaTeX: Force symmetric margins
\usepackage[left=2.5cm, right=2.5cm, top=2.5cm, bottom=2.5cm]{geometry}
% ❌ WRONG: left=3cm, right=1.5cm
```
---
## 2. Text Overflow: Font Metrics Pre-Calculation
### The Wrong Way
```python
# ❌ NEVER estimate by character count
if len(text) > 20:
wrap() # Wrong — 20 CJK chars ≠ 20 Latin chars ≠ 20 mixed chars
```
### The Right Way — ReportLab
```python
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus import Paragraph
from reportlab.lib.styles import ParagraphStyle
# Measure actual rendered width
text_width = stringWidth("Your text here", "Noto Sans SC", 10)
if text_width > max_width:
# Use Paragraph for automatic wrapping — NEVER plain strings in tables
style = ParagraphStyle(
'CellText',
fontName='Noto Sans SC',
fontSize=10,
leading=14,
wordWrap='CJK', # Enables CJK-aware line breaking
)
element = Paragraph(text, style)
else:
element = text # Plain string is fine if it fits
```
### Key Rules
- **Always use `Paragraph()` for table cell content** — plain strings don't wrap and will overflow
- **CJK text is wider**: Budget ~12pt per character at 10pt font size (vs ~6pt for Latin)
- **URLs and long strings**: If a single "word" exceeds column width, enable `wordWrap='CJK'` or split manually
- **Hyphenation**: For English text, consider `pyphen` for proper hyphenation of long words
### The Right Way — LaTeX
```latex
% Use tabularx for auto-wrapping columns
\usepackage{tabularx}
\begin{tabularx}{\columnwidth}{lXX} % X columns auto-wrap
Header 1 & This long text will wrap & Another wrapping column \\
\end{tabularx}
% For URLs
\usepackage{url}
\url{https://very-long-url-that-would-overflow.example.com/path/to/resource}
```
### The Right Way — Playwright/HTML
```css
/* Global text overflow prevention */
p, td, li, .content {
overflow-wrap: break-word;
word-break: break-word;
hyphens: auto;
}
/* Strict CJK line-break rules */
body {
line-break: strict;
word-break: normal;
}
/* Table cells must constrain content */
/* ⚠️ overflow:hidden + ellipsis only for single-line short text cells.
Multi-line td should use overflow-wrap: break-word, not overflow: hidden,
otherwise text gets truncated. Especially important in Playwright PDFs. */
td {
max-width: 0; /* Forces column to respect assigned width */
overflow: hidden;
text-overflow: ellipsis; /* Only for single-line cells */
}
```
---
## 3. Image & Chart Overflow: Proportional Scaling
### Absolute Rule
> **Never insert an image or chart at its original dimensions. Always compute fit-to-container scaling.**
### ReportLab Pattern
```python
from reportlab.platypus import Image
from reportlab.lib.units import mm
def fit_image(img_path, max_w, max_h):
"""Scale image to fit within max_w × max_h, preserving aspect ratio."""
img = Image(img_path)
orig_w, orig_h = img.drawWidth, img.drawHeight
ratio_w = max_w / orig_w if orig_w > max_w else 1.0
ratio_h = max_h / orig_h if orig_h > max_h else 1.0
ratio = min(ratio_w, ratio_h)
img.drawWidth = orig_w * ratio
img.drawHeight = orig_h * ratio
return img
# Usage
available_width = page_width - left_margin - right_margin
max_img_height = A4[1] * 0.35 # ~294pt ≈ 10cm — prevents image from eating page + leaves room for caption
img = fit_image("chart.png", available_width, max_img_height)
story.append(img)
```
### LaTeX Pattern
```latex
\usepackage{adjustbox}
% Always constrain to column width
\includegraphics[max width=\columnwidth]{chart.png}
% Or with adjustbox for both dimensions
\begin{adjustbox}{max width=\columnwidth, max height=0.4\textheight}
\includegraphics{chart.png}
\end{adjustbox}
```
### Playwright/HTML Pattern
```css
img, svg, .chart-container {
max-width: 100%;
max-height: 45vh; /* Prevent one image from eating an entire page */
height: auto;
object-fit: contain; /* Preserve aspect ratio */
}
```
> **Why `max-height: 45vh`?** Without a height cap, a tall image combined with `break-inside: avoid` (from pagination.md) gets pushed to the next page — leaving the current page mostly empty and the image occupying an entire page alone. 45vh ensures any image fits within half a page, leaving room for surrounding text on the same page.
---
## 3.5 Horizontal Flex/Inline Layout Overflow (Flow Bars, Step Lists, Tag Rows)
**Problem:** LLMs commonly generate horizontal `display: flex` layouts (process flow bars, step indicators, tag rows, icon grids) without any width constraint or wrap control. When content is longer than expected (e.g. "Theory Framework (ASPICE / V-Model)" as a step label), the total width exceeds the container, pushing content beyond the right page boundary.
**Playwright PDF consequence:** When any element causes `scrollWidth > clientWidth`, Playwright shrinks the **entire page** to fit, causing all content to appear left-shifted with blank space on the right. This affects ALL pages, not just the one with the overflow.
### Iron Rules (Direct HTML Flow)
**Rule 3.5.1 — Mandatory `flex-wrap` for ≥3 inline items:**
```css
/* Any horizontal row with 3+ children MUST have flex-wrap */
.flow-bar, .step-row, .tag-row, .icon-grid {
display: flex;
flex-wrap: wrap; /* MANDATORY for 3+ items */
gap: 12px; /* Consistent spacing */
max-width: 100%; /* Never exceed container */
}
```
**Rule 3.5.2 — Flex children must have `min-width` + `flex-shrink`:**
```css
.flow-step, .tag-item {
flex: 1 1 auto; /* Grow, shrink, auto basis */
min-width: 80px; /* Prevent crushing to 0 */
max-width: 100%; /* Never exceed container alone */
overflow-wrap: break-word; /* Break long words */
word-break: break-all; /* CJK fallback */
}
```
**Rule 3.5.3 — Arrow/connector separators must not be rigid:**
```css
/* ❌ WRONG — rigid arrow div between flex items */
<div class="step">Step 1</div>
<div class="arrow">→</div> /* Fixed-width, prevents shrinking */
<div class="step">Step 2</div>
/* ✅ RIGHT — arrow as pseudo-element, doesn't affect flex layout */
.flow-step + .flow-step::before {
content: '→';
margin: 0 8px;
color: #999;
flex-shrink: 0;
}
```
**Rule 3.5.4 — Threshold-based layout switching:**
| Item count | Recommended layout | Notes |
|------------|-------------------|-------|
| 1-3 items | Horizontal flex (no wrap needed if items are short) | Still add `max-width: 100%` on container |
| 4-6 items | Horizontal flex + `flex-wrap: wrap` | Items may wrap to 2 rows |
| 7+ items | Vertical stack or CSS Grid 2×N | Horizontal becomes unreadable |
| Items with long text (>15 CJK chars / >25 Latin chars) | Vertical stack regardless of count | Long labels don't fit side-by-side |
### Quick Self-Check
Before generating any horizontal flex layout, verify:
```
□ Container has max-width: 100% (or explicit width ≤ page width)?
□ flex-wrap: wrap is set (if ≥3 items)?
□ Each child has min-width + max-width constraints?
□ Separators (arrows, dots, lines) are pseudo-elements, not rigid divs?
□ Long text items have overflow-wrap: break-word?
```
---
## 4. Table Overflow: Dynamic Column Width Allocation
Tables are the #1 source of horizontal overflow.
### Strategy: Weight-Based Column Width
```python
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.lib.styles import ParagraphStyle
def calculate_col_widths(data, font_name, font_size, available_width, min_col=30):
"""Calculate column widths based on content weight.
Each column's width is proportional to its widest content,
with a minimum width and total constrained to available_width.
"""
n_cols = len(data[0])
# Measure max content width per column
max_widths = [0] * n_cols
for row in data:
for i, cell in enumerate(row):
text = str(cell) if not isinstance(cell, Paragraph) else cell.text
w = stringWidth(text, font_name, font_size) + 8 # +8pt padding
max_widths[i] = max(max_widths[i], w)
total_natural = sum(max_widths)
if total_natural <= available_width:
# Everything fits — distribute remaining space proportionally
extra = available_width - total_natural
return [w + extra * (w / total_natural) for w in max_widths]
else:
# Must compress — allocate proportionally with minimum
col_widths = []
for w in max_widths:
allocated = max(min_col, available_width * (w / total_natural))
col_widths.append(allocated)
# Normalize to exactly fit available_width
scale = available_width / sum(col_widths)
return [w * scale for w in col_widths]
def build_safe_table(data, available_width, font_name='Noto Sans SC', font_size=9):
"""Build a table guaranteed not to overflow horizontally.
All text cells are wrapped in Paragraph() for automatic line-breaking.
"""
wrap_style = ParagraphStyle(
'TableCell',
fontName=font_name,
fontSize=font_size,
leading=font_size + 3,
wordWrap='CJK',
)
# Wrap all cells in Paragraph
wrapped_data = []
for row in data:
wrapped_row = [Paragraph(str(cell), wrap_style) for cell in row]
wrapped_data.append(wrapped_row)
col_widths = calculate_col_widths(data, font_name, font_size, available_width)
# Verify total width
assert sum(col_widths) <= available_width + 0.5, \
f"Table width {sum(col_widths):.1f} exceeds available {available_width:.1f}"
table = Table(wrapped_data, colWidths=col_widths, repeatRows=1)
return table
```
### LaTeX Table Width Management
```latex
% For tables that MUST fit single column
\begin{tabularx}{\columnwidth}{l X X r} % X = flexible width columns
...
\end{tabularx}
% For wide tables in twocolumn mode → span full page
\begin{table*}[t]
\begin{tabularx}{\textwidth}{l X X X X r}
...
\end{tabularx}
\end{table*}
% Last resort: shrink to fit (verify ≥ 6pt after scaling)
\resizebox{\columnwidth}{!}{%
\begin{tabular}{lllllll}
...
\end{tabular}
}
```
### LaTeX Equation Width Management (Dual-Column)
Equations are the **#2 overflow source** after tables in two-column papers (ACM `sigconf` column ~241pt, IEEE ~252pt).
```latex
% ❌ WRONG — two full equations on one line
\begin{equation}
\mathbf{e}_u = \sum \frac{1}{\sqrt{...}} \mathbf{e}_i, \quad
\mathbf{e}_i = \sum \frac{1}{\sqrt{...}} \mathbf{e}_u
\end{equation}
% ✅ CORRECT — one equation per line
\begin{align}
\mathbf{e}_u &= \sum \frac{1}{\sqrt{...}} \mathbf{e}_i, \\
\mathbf{e}_i &= \sum \frac{1}{\sqrt{...}} \mathbf{e}_u.
\end{align}
% ✅ For wide fractions (softmax, attention)
% Factor out sub-expressions into separate definitions
\begin{equation}
\alpha_{uv} = \frac{\exp(f(u,v))}{\sum_k \exp(f(u,k))},
\quad \text{where } f(u,v) = \text{LeakyReLU}(\ldots)
\end{equation}
% ✅ For contrastive losses: use multline
\begin{multline}
\mathcal{L}_{\text{SSL}}^u =
-\log \frac{\exp(\text{sim}(z_u', z_u'')/\tau)}
{\sum_{v \neq u} \exp(\text{sim}(z_u', z_v'')/\tau)}.
\end{multline}
```
**Quick heuristic:** if `equation` body > 60 raw characters (excluding `\label`), it probably overflows dual-column. Use `align`, `split`, `multline`, or factor out sub-expressions.
See `academic.md` Rules M1–M4 for full patterns.
### LaTeX Algorithm Width Management (Dual-Column)
```latex
\SetAlFnt{\small} % ❗ MANDATORY in dual-column
\SetAlCapFnt{\small}
% Break long Input/Output across lines:
\KwInput{Graph $\mathcal{G}_R$, $\mathcal{G}_S$\\
\quad dim $d$, layers $L$, lr $\eta$, reg $\lambda$}
% Or use algorithm* to span full width
\begin{algorithm*}[t] ... \end{algorithm*}
```
### ⚠️ `\columnwidth` vs `\textwidth` in Two-Column Layouts
| Context | `\columnwidth` | `\textwidth` |
|---------|---------------|-------------|
| Single-column doc | = page content width | = page content width (same) |
| Two-column doc (`table` float) | = **one column** (~252pt) | = **full page** (~504pt) |
| Two-column doc (`table*` float) | = one column | = full page |
**Rule:** Inside `table` (single-col float), ALWAYS use `\columnwidth`. Inside `table*` (full-width float), use `\textwidth`.
`check-tex` detects `\resizebox{\textwidth}` inside single-column floats as error `RESIZEBOX_TEXTWIDTH`.
---
## 5. Fallback & Degradation Strategies
When content doesn't fit even after wrapping and scaling, apply these strategies **in order**:
### Automatic Degradation Ladder
| Step | Strategy | Limit | Notes |
|------|----------|-------|-------|
| 1 | Wrap text into Paragraph | — | Always do this first |
| 2 | Shrink font by 1pt | **Min 14pt** (single-col) / **12pt** (dual-col) | ⚠️ Enforced by `fill-engine.md` Safety Net 1 |
| 3 | Reduce padding/spacing | Min 4pt padding | Don't go below 4pt cell padding |
| 4 | Switch to landscape | Only if user allows | Never change orientation silently |
| 5 | Split into multiple elements | — | e.g., one wide table → two tables |
| 6 | Log warning + render anyway | — | If all else fails, at least don't crash |
### ReportLab Font Degradation
```python
def fit_text_with_degradation(text, font_name, base_size, max_width, min_size=14):
"""Try progressively smaller font sizes until text fits.
NOTE: min_size enforced by fill-engine.md Safety Net 1.
Single-column: min_size=14. Dual-column: min_size=12.
If text still doesn't fit at min_size → trigger page break, do NOT shrink further.
"""
for size in range(base_size, min_size - 1, -1):
if stringWidth(text, font_name, size) <= max_width:
return size
return min_size # Absolute floor — log warning
```
### Table Column Degradation
```python
def degrade_table_if_needed(data, available_width, font_name, base_font_size=10):
"""Try fitting table, degrading font size if needed."""
for font_size in [base_font_size, base_font_size - 1, base_font_size - 2]:
col_widths = calculate_col_widths(data, font_name, font_size, available_width)
if all(w >= 25 for w in col_widths): # Minimum 25pt per column
return font_size, col_widths
# Still doesn't fit — consider splitting table or landscape
return base_font_size - 2, col_widths
```
---
## 6. Vertical Overflow: Y-Cursor & Smart Pagination
Horizontal overflow → wrap/scale/shrink.
Vertical overflow → paginate.
### Y-Cursor Architecture (ReportLab Platypus handles this, but understand it)
```
Page Start
├── Current_Y = top_margin
├── Draw Block A (height = 80pt)
│ └── Current_Y += 80 + spacing
├── Draw Block B (height = 120pt)
│ └── Current_Y += 120 + spacing
├── Check: Current_Y + Next_Block_Height > (page_height - bottom_margin)?
│ ├── YES → New page, reset Current_Y = top_margin
│ └── NO → Continue drawing
└── ...
```
### Anti-Tear Rules (Elements That Must Not Split)
```python
from reportlab.platypus import KeepTogether
# 1. Heading + first paragraph — MANDATORY
story.append(KeepTogether([
heading,
first_paragraph,
]))
# 2. Image/chart + caption — MANDATORY
story.append(KeepTogether([
chart_image,
caption_paragraph,
]))
# 3. Table title + table (short tables ≤ 15 rows)
if len(data) <= 15:
story.append(KeepTogether([table_title, table]))
# 4. Long tables: repeat header on each page
table = Table(data, repeatRows=1) # First row repeats on every page
```
### Orphan/Widow Prevention
- If a paragraph's last line would be alone on the next page → pull at least 2 lines forward
- If a section heading lands at page bottom with no body text → push to next page
- ReportLab: `KeepTogether` handles most cases; `allowSplitting=False` for critical blocks
### LaTeX Vertical Overflow
```latex
% Prevent orphans and widows
\widowpenalty=10000
\clubpenalty=10000
% Prevent page break after heading
\usepackage{titlesec}
\titlespacing*{\section}{0pt}{12pt plus 4pt minus 2pt}{6pt plus 2pt minus 2pt}
% Keep float near text
\usepackage[section]{placeins} % \FloatBarrier at each \section
```
---
## 7. Two-Pass Rendering (Advanced — for Complex Documents)
For critical documents where overflow would be unacceptable:
### Pass 1: Virtual Layout (Measurement)
Calculate all element sizes without rendering. Build a **Layout Tree**:
```python
layout_tree = [
{"type": "heading", "width": 450, "height": 28, "page": 1},
{"type": "paragraph", "width": 450, "height": 84, "page": 1},
{"type": "table", "width": 450, "height": 220, "page": 1},
{"type": "chart", "width": 400, "height": 300, "page": 2},
# ...
]
```
### Collision Detection
```python
def check_overflow(layout_tree, page_width, left_margin, right_margin):
"""Verify no element overflows page boundaries."""
max_x = page_width - right_margin
violations = []
for elem in layout_tree:
right_edge = left_margin + elem["width"]
if right_edge > max_x:
violations.append({
"element": elem["type"],
"page": elem["page"],
"overflow_by": right_edge - max_x,
})
return violations
```
### Pass 2: Render with Confirmed Layout
Only render after Pass 1 confirms zero violations. If violations found → apply degradation strategies from §5, then re-run Pass 1.
**In practice**: ReportLab's Platypus engine already does a form of two-pass rendering internally (`doc.multiBuild()`). Use `multiBuild` + `afterFlowable` callbacks for complex documents that need cross-referencing or dynamic layout adjustment.
---
## Quick Reference: Which Route Uses What
| Mechanism | ReportLab (Report) | LaTeX (Academic) | Playwright (Creative) |
|-----------|-------------------|-------------------|----------------------|
| Text wrapping | `Paragraph()` + `wordWrap='CJK'` | `tabularx` X columns | CSS `overflow-wrap: break-word` |
| Image scaling | `fit_image()` helper | `\includegraphics[max width=]` | CSS `max-width: 100%` |
| Table width | `calculate_col_widths()` | `tabularx` / `resizebox` | CSS `table-layout: fixed` |
| Font degradation | `fit_text_with_degradation()` | `\small` / `\footnotesize` | CSS `font-size` step-down |
| Page break | `PageBreak()` / `KeepTogether` | `\newpage` / `\FloatBarrier` | CSS `break-before: page` |
| Header repeat | `Table(repeatRows=1)` | `\endhead` in longtable | `thead { display: table-header-group }` |
| Orphan/widow | `KeepTogether` | `\widowpenalty=10000` | CSS `orphans: 2; widows: 2` |
---
## Checklist (Run Before Every PDF Build)
```
□ All table cells use Paragraph() wrapping, not plain strings?
□ sum(colWidths) ≤ available_width verified in code?
□ Images scaled to fit container (not original size)?
□ Long tables have repeatRows=1 (or thead header-group)?
□ Heading + first paragraph wrapped in KeepTogether?
□ Chart + caption wrapped in KeepTogether?
□ CJK text uses wordWrap='CJK' style?
□ URL/long-string cells have word-break handling?
□ Font degradation fallback exists for tight columns?
□ Last page content ratio ≥ 40%?
```
+367
View File
@@ -0,0 +1,367 @@
# Pagination & Flow Control
> Core rules for multi-page document layout quality. Must be followed every time a multi-page PDF is generated.
>
> Related:
> - `typesetting/overflow.md` — the comprehensive overflow prevention system covering all three routes (ReportLab, LaTeX, Playwright).
> - `typesetting/fill-engine.md` — the **anti-void** adaptive engine (handles pages with too little content: font floor, fill ratio, paragraph inflation, Y-axis golden-ratio anchoring).
---
## 1. Last Page Blank Control (Anti-Orphan Page)
**Problem**: The last page has only one or two lines of content with large blank areas — looks terrible.
**Mandatory Rules**:
- After generating multi-page content, **you must check the content fill ratio of the last page**
- When last page content ratio < 25%, **you must backtrack and adjust**
- Adjustment strategies (by priority):
1. **Compress preceding page spacing**: Reduce margin-bottom between sections (decrease 2-4px each)
2. **Tighten line height**: Body line-height from 1.7 → 1.55 (no lower than 1.5)
3. **Reduce font size**: Body from 16px → 15px (no lower than 14px)
4. **Trim content**: Remove dispensable descriptive text without affecting core information
5. **Merge small sections**: Combine adjacent sections with little content
**Checking Method (Playwright HTML route)**:
```css
/* Check min-content on the last .page element */
/* If content is less than 25% of page, backtracking is needed */
```
**Practical Standards**:
- Last page content ratio >= 40% → ✅ Pass
- Last page content ratio 25%-40% → ⚠️ Acceptable but optimization recommended
- Last page content ratio < 25% → ❌ Must adjust
---
## 2. Table Cross-Page Integrity
**Problem**: Table header and first data row split across two pages; table cut in the middle.
**Mandatory Rules**:
### Playwright HTML Route
```css
/* Prevent table splitting */
table, .table-wrapper {
break-inside: avoid; /* Preferred: keep entire table together */
page-break-inside: avoid;
}
/* If table is too long and must split, ensure header repeats */
thead {
display: table-header-group; /* Repeat header on each page */
}
/* Don't cut table rows in the middle */
tr {
break-inside: avoid;
page-break-inside: avoid;
}
/* Bind header + at least 2 data rows together */
thead + tbody tr:first-child,
thead + tbody tr:nth-child(2) {
break-before: avoid;
page-break-before: avoid;
}
```
### ReportLab Route
```python
# Use Table's repeatRows parameter
table = Table(data, repeatRows=1) # Repeat header on each page
# Or use KeepTogether to wrap small tables
from reportlab.platypus import KeepTogether
elements.append(KeepTogether([table_title, table]))
```
**Additional Rules**:
- Table rows ≤ 8: Entire table `break-inside: avoid`, no page splitting allowed
- Table rows > 8: Splitting allowed, but must use `thead { display: table-header-group }` to repeat header on each page
- All card-grid / flex-grid layouts follow the same rule: `break-inside: avoid`
---
## 3. CJK Punctuation Placement Rules
**Problem**: Commas, periods, enumeration commas, etc. appearing at line start, violating CJK typesetting standards.
**Mandatory Rules**:
### Playwright HTML Route (Recommended)
```css
/* Global CJK punctuation rules */
body {
line-break: strict; /* Strict line-break rules */
word-break: normal; /* Don't force word breaks */
overflow-wrap: break-word; /* Allow long words to break */
hanging-punctuation: allow-end; /* Allow punctuation to hang past line end */
}
/* For body paragraphs */
p, .body-text, td, li {
line-break: strict;
text-align: justify; /* Justify to reduce line-end gaps */
}
```
**Effect of `line-break: strict`**:
- Prevents line-start: ,。、;:!?)】》…—
- Prevents line-end: (【《
- Natively supported by Chromium engine, no extra JS needed
### ReportLab Route
```python
# Set in ReportLab Paragraph style
from reportlab.lib.enums import TA_JUSTIFY
style = ParagraphStyle(
'Body',
alignment=TA_JUSTIFY,
wordWrap='CJK', # CJK line-break mode
)
```
**Verification Checklist**:
- [ ] No comma/period appears as the first character of any line
- [ ] Left parenthesis / left quotation mark does not appear at line end
- [ ] Ellipsis is not broken in the middle
---
## 4. Major Section Page-Break Rule (3/4 Threshold)
**Problem**: A major section (H1/一级标题) ends at ~75% of the page, and the next major section’s title gets squeezed into the remaining 25%. This looks cramped and ugly — the new section deserves a fresh page.
**Iron Rule**: When a major section (H1-level heading, e.g., “一、”“二、” or “Chapter 1”) is about to start, check remaining page space:
| Remaining space | Action |
|----------------|--------|
| **≥ 25% of page height** | Continue on same page — enough room for heading + meaningful content |
| **< 25% of page height** | Force page break — start the new section on a fresh page |
**Why 25% (not 50%)?** A major heading needs at least its title + 2-3 lines of body text to look intentional. If there’s only enough room for a title and a line or two, it looks like an accident.
### ReportLab Implementation
```python
from reportlab.platypus import CondPageBreak
# Before every H1-level heading, insert a conditional page break.
# CondPageBreak(height) breaks to next page if remaining space < height.
# Use 75% of available page height as threshold.
available_height = page_height - top_margin - bottom_margin
threshold = available_height * 0.25 # break if less than 25% remains
# In story building:
story.append(CondPageBreak(threshold)) # ← goes before H1 heading
story.append(h1_paragraph)
```
### Playwright / CSS @page Implementation
```css
/* H1-level headings always prefer starting on a new page
unless there's substantial room remaining */
h1, .major-section-title {
break-before: auto; /* Default: don't force */
page-break-before: auto;
}
```
```javascript
// Post-render check: if an H1 starts in the bottom 25% of the viewport,
// force a page-break-before to avoid orphan headings
document.querySelectorAll('h1, .major-section-title').forEach(h => {
const rect = h.getBoundingClientRect();
// In print context, check if heading is too far down the page
// This can be verified after Playwright render via page.evaluate()
const pageHeight = window.innerHeight;
const relativeY = rect.top / pageHeight;
if (relativeY > 0.75) {
h.style.breakBefore = 'page';
}
});
```
```
### LaTeX Implementation
```latex
% Before each \section{} (H1-level), check remaining space
\needspace{0.25\textheight} % requires needspace package
\section{New Major Section}
```
**Scope**: This rule applies to **H1-level headings only** (major sections, chapters, top-level numbered items like “一、”“二、”). Sub-sections (H2, H3) follow the standard heading-body binding rule (no orphan headings at page bottom) but do NOT force page breaks.
---
## 5. Other Anti-Split Rules
### Heading–Body Binding
```css
h1, h2, h3, h4, .section-title {
break-after: avoid; /* Don't page-break after heading */
page-break-after: avoid;
}
```
### Image / Card Protection
```css
figure, .card, .kpi-card, .project-card {
break-inside: avoid;
page-break-inside: avoid;
}
```
> **⚠️ Image `max-height` is critical.** `break-inside: avoid` alone can cause images to occupy an entire page when the image is tall. Always pair with `max-height` from overflow.md (`img { max-height: 45vh }`) to prevent single images from consuming a full page.
### List Item Binding
```css
li {
break-inside: avoid;
}
/* Keep at least 2 list items on the same page */
li:last-child {
break-before: avoid;
}
```
---
## Quick Checklist (After every multi-page PDF generation)
```
□ Last page content ≥ 40%?
□ Major sections (H1) not starting in bottom 25% of a page?
□ Table header and data rows not separated?
□ No punctuation appearing at line start?
□ No heading orphaned at page bottom?
□ No card/image cut in half?
□ Page numbering follows the standard scheme (see Section 6)?
```
---
## 6. Standard Page Numbering Scheme
All multi-page documents MUST follow this five-zone page numbering convention unless the user explicitly requests otherwise.
### Zone Definitions
| Zone | Section | Numbering Style | Starts At | Visibility |
|------|---------|----------------|-----------|------------|
| **1. Cover** | Title page | — | Logical page 1 | **Hidden** (no visible page number, but counts as page 1 internally) |
| **2. Front Matter** | Table of Contents, Preface, Abstract, Acknowledgments | **Lowercase Roman** (i, ii, iii, iv, v…) | i | Visible, centered footer |
| **3. Body** | Main content chapters/sections | **Arabic** (1, 2, 3…) | **Resets to 1** | Visible, centered or outer-edge footer |
| **4. Appendix** | Appendices (A, B, C…) | **Arabic, continues** from body | Continues | Visible |
| **5. References / Bibliography** | Works cited, bibliography | **Arabic, continues** from body/appendix | Continues | Visible |
### Key Rules
0. **NEVER use "Page X of Y" format (denominator is FORBIDDEN).** Footer must show only the page number itself (e.g., `1`, `2`, `iii`). Do NOT display total page count. No `Page 3 of 12`, no `第3页/共12页`, no `3 / 12`. Just the bare number.
1. **Cover page is ALWAYS page 1 internally** but the page number is **never displayed**. This is achieved by suppressing the footer/header on the first page, not by excluding it from the page count.
2. **Front matter uses a separate Roman numeral sequence.** When front matter exists (TOC, abstract, preface), it forms its own numbering sequence starting at `i`. This sequence is independent of the body numbering.
3. **Body numbering resets to Arabic 1.** The first page of actual content (Chapter 1, Introduction, etc.) is always page `1` regardless of how many front matter pages precede it.
4. **Appendix and references continue the body sequence.** There is NO reset between body → appendix → references. If the body ends on page 42, Appendix A starts on page 43.
5. **Documents without front matter** skip zone 2 entirely. Cover = hidden page 1, body starts at visible page 1.
6. **Documents without a cover** start the body (or front matter if present) at page 1 directly.
### ReportLab Implementation
```python
from reportlab.platypus import SimpleDocTemplate, PageBreak, NextPageTemplate, PageTemplate
from reportlab.lib.units import inch
from reportlab.platypus.frames import Frame
def footer_with_arabic(canvas, doc):
"""Standard Arabic page number in footer."""
canvas.saveState()
canvas.setFont('FreeSerif', 9)
canvas.drawCentredString(doc.pagesize[0] / 2, 0.5 * inch,
str(doc.page))
canvas.restoreState()
def footer_with_roman(canvas, doc):
"""Roman numeral page number for front matter."""
roman_map = {1:'i',2:'ii',3:'iii',4:'iv',5:'v',6:'vi',7:'vii',8:'viii',9:'ix',10:'x'}
page_num = roman_map.get(doc.page, str(doc.page))
canvas.saveState()
canvas.setFont('FreeSerif', 9)
canvas.drawCentredString(doc.pagesize[0] / 2, 0.5 * inch, page_num)
canvas.restoreState()
def no_footer(canvas, doc):
"""Cover page — no visible page number."""
pass
# Define page templates:
# - 'cover': no footer
# - 'frontmatter': Roman numeral footer
# - 'body': Arabic footer (page counter resets)
```
### Playwright / HTML + CSS Implementation
```css
/* Zone 1: Cover — suppress page number */
.page-cover {
/* No footer content */
}
/* Zone 2: Front matter — Roman numerals via CSS counter */
@page :nth(2) { /* Adjust range based on front matter pages */ }
/* For Playwright, page numbers are typically added via:
1. A footer element on each .page div, or
2. Post-processing with pypdf after PDF generation */
```
**Practical approach for Playwright route**: Since CSS `@page` counters with Roman/Arabic switching are poorly supported, the recommended pattern is:
1. Generate the PDF without page numbers
2. Use pypdf to stamp page numbers in post-processing:
- Skip page 1 (cover)
- Roman numerals for front matter pages
- Arabic starting from 1 for body pages
### LaTeX Implementation
```latex
% Cover: no page number displayed
\begin{titlepage}
\thispagestyle{empty} % Suppress page number
% ... cover content ...
\end{titlepage}
% Front matter: Roman numerals
\pagenumbering{roman} % Switches to i, ii, iii...
\tableofcontents
\newpage
% Body: Arabic, reset to 1
\pagenumbering{arabic} % Switches to 1, 2, 3... (auto-resets counter)
\section{Introduction}
% ...
% Appendix: continues Arabic numbering (no reset)
\appendix
\section{Appendix A} % Page number continues from body
% References: continues Arabic numbering (no reset)
\bibliographystyle{plain}
\bibliography{refs}
```
### When to Deviate
- **Single-page documents** (certificates, letters, posters): No page numbering at all.
- **Short documents (≤3 pages)**: Simple Arabic `1, 2, 3` throughout, no cover/frontmatter distinction.
- **User explicitly requests a different scheme**: Follow the user's instructions.
- **Exam papers**: Sequential Arabic numbering on every page, including page 1.
+252
View File
@@ -0,0 +1,252 @@
# Color Palette System
> Color is the skeleton of design. Unified, restrained, systematic. Garish = amateur.
---
## Cascade Palette System (V2 — Preferred)
The cascade palette enforces one iron law: **Area ∝ 1/Saturation**.
The larger the colored area, the lower its saturation must be.
### Tier System
| Tier | Area % | S Cap | Roles |
|------|--------|-------|-------|
| **XL** | >50% | ≤ 0.08 | `page_bg`, `section_bg` |
| **L** | 20-50% | ≤ 0.15 | `card_bg`, `table_stripe` |
| **M** | 5-20% | ≤ 0.30 | `header_fill`, `cover_block` |
| **S** | 1-5% | ≤ 0.50 | `border`, `icon` |
| **XS** | <1% | ≤ 0.75 | `accent`, `accent_secondary` |
### How It Works
One base hue → 12 roles + 4 semantic colors. Cover, body, and charts all pull from the same palette:
```
palette.cascade
├── cover subset (page_bg, header_fill, cover_block, accent, text_primary...)
├── body subset (page_bg, section_bg, card_bg, table_stripe, border...)
├── chart subset (accent as series_1, accent_secondary as series_2, ...)
└── semantic (success, warning, error, info — all low-sat)
```
No orphan colors. No "cover finished, now pick new colors for body" drift.
### Primary Accent Hue Rule (V2.1)
**`accent` (XS tier) MUST use `base_hue`** — the same hue as `header_fill`, `cover_block`, `border`, `icon`, and all other structural roles. Only the saturation and lightness differ (S=0.50-0.70, L=0.40-0.55 for light modes).
This ensures that cover structural lines, body H2 headings, table headers, and accent highlights all visibly belong to the **same color family**.
`accent_secondary` uses the geometric harmony hue (`accent_hue`) for chart series differentiation — this is the **only** role that may deviate from `base_hue`.
```
base_hue (e.g. 200° steel blue)
├── header_fill: H=200°, S=0.22, L=0.25 (M tier, structural)
├── accent: H=200°, S=0.65, L=0.45 (XS tier, same hue, higher sat)
└── accent_2: H=350°, S=0.50, L=0.50 (XS tier, geometric hue for charts)
```
### Usage
```bash
# Via design_engine.py
python3 "$PDF_SKILL_DIR/scripts/design_engine.py" palette-cascade --intent cold --mode minimal
# Via pdf.py (auto-derives intent from title)
python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.cascade --title "2025年度报告" --format reportlab
# Formats: summary (default) | json | css | reportlab
```
### Output Formats
- **summary**: Human-readable table with tier/role/hex/saturation
- **json**: Full structured data (roles, cover, body, charts, semantic, meta)
- **css**: CSS custom properties ready for HTML/Playwright
- **reportlab**: Python code ready to paste into ReportLab scripts
---
## Core Iron Rules
### 1. One Document, One Color Family
**Not one color — one color family.**
- After choosing the primary color, all secondary, accent, and background colors must be derived from it
- **`accent` must share the same hue as `header_fill`** — only saturation and lightness differ
- Derivation methods: lightness shift, saturation shift, micro hue adjustment (within ±15°)
- **Forbidden** to have unrelated colors in the same document
- The only exception is `accent_secondary` which may use a geometric harmony hue for chart differentiation
```
Primary → Headings, key data, primary buttons
Secondary → Primary lightness ±15-25%
Accent → Primary hue ±10-15°, for highlights/warnings
Neutral → Gray series for body text, not conflicting with primary
Background → Pure white / primary at opacity 3-8%
```
### 2. Color Count Limits
| Element Type | Max Colors | Notes |
|-------------|-----------|-------|
| Entire document | 4-5 | Primary + secondary + accent + neutral + background |
| Single component (card/table) | 2-3 | Don't give each card a different color |
| Charts / data visualization | Same-family gradient | Differentiate by opacity/lightness, not different hues |
| Tags / badges | 1 color + text color | No rainbow tags |
### 3. Table Color Rules (Iron Law)
Tables are M+L tier elements. Their colors **must** come from the cascade palette's body_subset:
| Table Element | Cascade Role | Tier | S Cap | ❌ Never Use |
|---------------|-------------|------|-------|------------|
| Header bg | `HEADER_FILL` | M | ≤ 0.30 | `ACCENT` (too vivid for header area) |
| Header text | `colors.white` | — | — | Colored text on colored bg |
| Even rows | `colors.white` | — | — | Colored fills |
| Odd rows | `TABLE_STRIPE` | L | ≤ 0.15 | High-saturation stripes |
| Borders/grid | `BORDER` | S | ≤ 0.50 | Pure black `#000` |
**Why this matters:** Table headers occupy ~5-20% of visual area. The cascade tier system says M-tier elements (5-20% area) must have S ≤ 0.30. Using `ACCENT` (XS tier, S = 0.50-0.75) on table headers violates the area∝1/saturation rule and creates jarring color blocks that clash with the surrounding low-saturation body.
**All tables in one document must use identical colors.** No per-table color themes.
### 3. Absolutely Forbidden Color Fills
The following are automatic failures:
- ❌ 4 cards using 4 completely different colors (red/blue/green/purple)
- ❌ Alternating table rows in different colors (blue row/pink row)
- ❌ Rainbow-colored pie charts/bar charts
- ❌ Each section with a different theme color
- ❌ Gradient transitioning from warm to cool tones (red → blue)
- ❌ Using `ACCENT` (XS tier) for table header backgrounds — use `HEADER_FILL` (M tier)
- ❌ Using any color outside the cascade palette for table styling
---
## Color Generation Rules
### Deriving a Full Palette from Primary
```
Given primary H(hue) S(saturation) L(lightness):
Primary: hsl(H, S, L) — Headings, key elements
Dark variant: hsl(H, S, L-15%) — Hover, borders, icons
Light variant: hsl(H, S-10%, L+25%) — Tag backgrounds, light fills
Ultra-light bg: hsl(H, S-20%, 96%) — Section backgrounds, card base
Accent: hsl(H+15, S, L) — Warnings, highlights (micro hue shift)
```
### Example: Deriving from a primary color
> ⚠️ The hex values below are **examples only**. In production, use `palette.cascade` or `palette.generate` to compute the full palette from intent.
```css
:root {
--c-primary: #2d5a87; /* Primary (from palette.cascade) */
--c-primary-d: #1e3d5c; /* Dark variant */
--c-primary-l: #5a8ab8; /* Light variant */
--c-primary-bg: #f0f4f8; /* Ultra-light background */
--c-accent: #2d6a87; /* Accent (hue +10°) */
--c-text: #333; /* Body text */
--c-text-muted: #888; /* Secondary text */
--c-border: #e0e4e8; /* Border lines */
}
```
---
## Multi-Element Differentiation Strategies
When distinguishing multiple sibling elements (e.g., multiple cards, categories), **don't use different colors — use these approaches instead**:
### Strategy A: Same Hue, Different Lightness
```css
.card-1 { background: hsl(220, 40%, 95%); } /* Lightest */
.card-2 { background: hsl(220, 40%, 90%); }
.card-3 { background: hsl(220, 40%, 85%); }
.card-4 { background: hsl(220, 40%, 80%); } /* Darkest */
```
### Strategy B: Same Color, Different Opacity
```css
.item-1 { background: rgba(30, 58, 95, 0.06); }
.item-2 { background: rgba(30, 58, 95, 0.12); }
.item-3 { background: rgba(30, 58, 95, 0.18); }
.item-4 { background: rgba(30, 58, 95, 0.24); }
```
### Strategy C: Primary + Whitespace + Lines
```css
/* Differentiate by border color/weight/style, uniform white background */
.card-1 { border-left: 3px solid var(--primary); }
.card-2 { border-left: 3px solid var(--primary-l); }
.card-3 { border-left: 3px solid var(--primary-d); }
```
### Strategy D: Icons / Numbering (Not Color)
```css
/* All cards same color, differentiated by icons, numbers, or layout variation */
```
---
## Gradient Usage Rules
### Allowed Gradients
- **Same-family gradient**: `linear-gradient(135deg, var(--c-primary), var(--c-primary-l))` — hue difference < 20°
- **Lightness gradient**: `linear-gradient(180deg, #fff, #f5f5f5)` — pure lightness change
- **Primary to transparent**: `linear-gradient(90deg, var(--c-primary), transparent)` — for decorative lines
### Forbidden Gradients
- ❌ Warm-to-cool crossover: `linear-gradient(#ff6b6b, #4ecdc4)`
- ❌ More than 3 colors: `linear-gradient(red, yellow, green, blue)`
- ❌ Neon gradients: Any high-saturation gradient
- ❌ Gratuitous gradients: Gradients added purely for "looks nice" without purpose
---
## Preset Palettes (Ready to Use)
### Business Blue
```
#1a365d → #2a5298 → #4a7ac7 → #dce6f5 → #f5f8fc
```
### Warm Gray
```
#2d2d2d → #5a5a5a → #8a8a8a → #e8e8e8 → #f9f9f9
```
### Forest Green
```
#1a3c2a → #2d6b4a → #4a9a6a → #d5ead8 → #f2f8f4
```
### Terracotta Red
```
#5c2018 → #8a3828 → #b85a48 → #f0d8d0 → #faf4f2
```
### Indigo Purple
```
#2d1b4e → #4a2d7a → #6a4aaa → #ddd0f0 → #f5f2fa
```
---
## Quick Check
```
□ How many colors does the entire document use? (Target ≤ 5)
□ Can every color be traced back to the primary?
□ Are there any colors that "suddenly appear" without derivation?
□ Are sibling elements rainbow-colored?
□ Gradient endpoint hue difference < 20°?
□ If you remove all color and look at grayscale only, is the hierarchy still clear?
```
+28
View File
@@ -0,0 +1,28 @@
---
## CJK Typography Supplement
> See `pagination.md` Section 3 for detailed rules.
### Scope: Creative Flow pipeline (html2pdf-next.js) ONLY
These CSS rules apply to **multi-page HTML documents** rendered via `html2pdf-next.js` (Creative Flow brief). In this pipeline, Chromium's `text-align: justify` + `line-break: strict` produces proper CJK typesetting with correct punctuation handling.
**Do NOT apply these rules to:**
- **Poster pipeline** (html2poster.js) — short lines + fixed containers make justify stretch CJK character spacing. Use `text-align: left` instead. See `briefs/poster.md` §7.1.
- **Report pipeline** (ReportLab) — use `TA_LEFT` for CJK body text. ReportLab’s justify algorithm handles CJK poorly. See `briefs/report.md` §CJK alignment.
```css
/* CJK punctuation placement rules — Creative Flow pipeline only */
body {
line-break: strict;
word-break: normal;
overflow-wrap: break-word;
}
p, td, li {
line-break: strict;
text-align: justify; /* Only for Creative Flow; poster/report use left */
}
```