Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi

This commit is contained in:
Kunthawat Greethong
2026-05-25 16:38:02 +07:00
commit 69f7d8bdda
1689 changed files with 342427 additions and 0 deletions

175
skills/assets/agency.md Normal file
View File

@@ -0,0 +1,175 @@
<!-- Updated: 2026-02-07 -->
# Agency/Consultancy SEO Strategy Template
## Industry Characteristics
- Service-based, high-value transactions
- Expertise and trust are paramount
- Long consideration cycles
- Portfolio/case study driven decisions
- Relationship-based sales
- Niche specialization benefits
## Recommended Site Architecture
```
/
├── Home
├── /services
│ ├── /service-1
│ │ ├── /sub-service-1
│ │ └── ...
│ └── /service-2
├── /industries
│ ├── /industry-1
│ ├── /industry-2
│ └── ...
├── /work (or /case-studies)
│ ├── /case-study-1
│ ├── /case-study-2
│ └── ...
├── /about
│ ├── /team
│ │ ├── /team-member-1
│ │ └── ...
│ ├── /culture
│ └── /careers
├── /insights (or /blog)
│ ├── /articles
│ ├── /guides
│ ├── /webinars
│ └── /podcasts
├── /contact
├── /process
└── /faq
```
## Schema Recommendations
| Page Type | Schema Types |
|-----------|-------------|
| Homepage | Organization, ProfessionalService |
| Service Page | Service, ProfessionalService |
| Case Study | Article, Organization (client) |
| Team Member | Person, ProfilePage |
| Blog | Article, BlogPosting |
### ProfessionalService Schema Example
```json
{
"@context": "https://schema.org",
"@type": "ProfessionalService",
"name": "Agency Name",
"description": "What the agency does",
"url": "https://example.com",
"logo": "https://example.com/logo.png",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Agency St",
"addressLocality": "City",
"addressRegion": "State",
"postalCode": "12345"
},
"telephone": "+1-555-555-5555",
"areaServed": "National",
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Services",
"itemListElement": [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Service 1"
}
}
]
}
}
```
## E-E-A-T Requirements
### Team Pages Must Include
- Professional headshots
- Detailed bios with credentials
- Industry experience
- Speaking engagements
- Publications
- Social profiles
### Case Studies Must Include
- Client name (with permission) or industry
- Challenge/problem statement
- Approach/methodology
- Results with specific metrics
- Timeline
- Testimonial quote
## Content Priorities
### High Priority
1. Service pages (detailed, specific)
2. Industry pages (vertical expertise)
3. 3-5 detailed case studies
4. Team/leadership pages
### Medium Priority
1. Methodology/process page
2. Blog with thought leadership
3. Comparison content (vs alternatives)
4. FAQ page
### Thought Leadership Topics
- Industry trend analysis
- How-to guides (non-competitive)
- Original research/surveys
- Event recaps and insights
- Expert interviews
- Tool/technology reviews
## Content Strategy
### Service Pages (min 800 words)
- Clear value proposition
- Methodology overview
- Deliverables list
- Relevant case studies
- Team members who deliver this service
- CTA to schedule consultation
### Industry Pages (min 800 words)
- Industry-specific challenges
- How you solve them differently
- Relevant case studies
- Industry credentials/experience
- Client logos (with permission)
### Case Studies (min 1,000 words)
- Executive summary
- Client background
- Challenge details
- Solution approach
- Implementation process
- Measurable results
- Client testimonial
- Related services/CTA
## Key Metrics to Track
- Organic traffic to service pages
- Case study page views
- Contact form submissions from organic
- Time on page for key content
- Blog → service page conversion
## Generative Engine Optimization (GEO) for Agencies
- [ ] Publish original case studies with specific, citable metrics and results
- [ ] Use Person schema with sameAs links for all team members (builds entity authority)
- [ ] Use ProfilePage schema for team member pages
- [ ] Include clear, quotable expertise statements in service page descriptions
- [ ] Produce original industry research and surveys AI systems can cite
- [ ] Structure thought leadership content with clear headings and extractable insights
- [ ] Maintain consistent agency entity information across directories, social profiles, and industry sites
- [ ] Monitor AI citation in ChatGPT, Perplexity, and Google AI Overviews for brand and key service terms

View File

@@ -0,0 +1,175 @@
/**
* AndroidFrame — Android设备边框参考Pixel 8系列
*
* 含punch-hole相机 + 状态栏 + 导航栏 + 圆角
*
* 用法:
* <AndroidFrame time="9:41" battery={85}>
* <YourAppContent />
* </AndroidFrame>
*/
const androidFrameStyles = {
wrapper: {
display: 'inline-block',
padding: 10,
background: '#1a1a1a',
borderRadius: 44,
boxShadow: '0 0 0 2px #2a2a2a, 0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
},
screen: {
position: 'relative',
borderRadius: 36,
overflow: 'hidden',
background: '#fff',
},
statusBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
fontSize: 14,
fontWeight: 500,
fontFamily: 'Roboto, -apple-system, sans-serif',
zIndex: 20,
pointerEvents: 'none',
},
punchHole: {
position: 'absolute',
top: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 14,
height: 14,
background: '#000',
borderRadius: '50%',
zIndex: 30,
},
statusIcons: {
display: 'flex',
alignItems: 'center',
gap: 6,
},
batteryText: {
fontSize: 11,
fontWeight: 600,
marginLeft: 2,
},
content: {
position: 'absolute',
top: 32,
left: 0,
right: 0,
bottom: 24,
overflow: 'auto',
},
navBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 60,
zIndex: 10,
},
navButton: {
width: 36,
height: 4,
background: 'rgba(0,0,0,0.3)',
borderRadius: 999,
},
};
function AndroidFrame({
children,
width = 412,
height = 892,
time = '9:41',
battery = 100,
darkMode = false,
navStyle = 'gesture',
}) {
const textColor = darkMode ? '#fff' : '#1a1a1a';
return (
<div style={androidFrameStyles.wrapper}>
<div style={{
...androidFrameStyles.screen,
width,
height,
background: darkMode ? '#000' : '#fff',
}}>
<div style={{ ...androidFrameStyles.statusBar, color: textColor }}>
<span>{time}</span>
<div style={androidFrameStyles.statusIcons}>
<svg width="14" height="10" viewBox="0 0 14 10" fill="currentColor">
<rect x="0" y="6" width="2" height="4" rx="0.5" />
<rect x="4" y="4" width="2" height="6" rx="0.5" />
<rect x="8" y="2" width="2" height="8" rx="0.5" />
<rect x="12" y="0" width="2" height="10" rx="0.5" />
</svg>
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
<path d="M7 9a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
<path d="M3 6a5 5 0 018 0" stroke="currentColor" strokeWidth="1.2" />
<path d="M0.5 3.5a11 11 0 0113 0" stroke="currentColor" strokeWidth="1.2" opacity="0.6" />
</svg>
<div style={{
width: 22,
height: 10,
border: '1.5px solid currentColor',
borderRadius: 2,
padding: 1,
position: 'relative',
}}>
<div style={{
width: `${battery}%`,
height: '100%',
background: 'currentColor',
borderRadius: 1,
}} />
</div>
<span style={androidFrameStyles.batteryText}>{battery}%</span>
</div>
</div>
<div style={androidFrameStyles.punchHole} />
<div style={androidFrameStyles.content}>
{children}
</div>
{navStyle === 'gesture' && (
<div style={androidFrameStyles.navBar}>
<div style={{
...androidFrameStyles.navButton,
width: 100,
height: 4,
background: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.4)',
}} />
</div>
)}
{navStyle === 'buttons' && (
<div style={androidFrameStyles.navBar}>
<span style={{ color: textColor, fontSize: 20 }}></span>
<span style={{ color: textColor, fontSize: 16 }}></span>
<span style={{ color: textColor, fontSize: 16 }}></span>
</div>
)}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.AndroidFrame = AndroidFrame;
}

View File

@@ -0,0 +1,340 @@
/**
* animations.jsx — 时间轴动画引擎
*
* Stage + Sprite 模式借鉴Remotion但轻量化。
*
* 导出(挂到 window.Animations
* - Stage: 整个动画容器,提供时间+控制
* - Sprite: 时间片段start/end内显示提供本地进度
* - useTime(): 读全局时间(秒)
* - useSprite(): 读本地进度 {t: 0→1, elapsed: seconds, duration: seconds}
* - Easing: {linear, easeIn, easeOut, easeInOut, spring, anticipation}
* - interpolate(t, [input0, input1], [output0, output1], easing?)
*
* 用法:
* <Stage duration={10}>
* <Sprite start={0} end={3}>
* <Title />
* </Sprite>
* <Sprite start={2} end={5}>
* <Subtitle />
* </Sprite>
* </Stage>
*
* 在Sprite子组件里用 useSprite() 读当前片段进度。
*/
(function() {
const { createContext, useContext, useState, useEffect, useRef, useCallback } = React;
const TimeContext = createContext({ time: 0, duration: 10, playing: false });
const SpriteContext = createContext(null);
const Easing = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => 1 - (1 - t) * (1 - t),
easeInOut: t => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
// expoOut: Anthropic-level 主 easing (cubic-bezier(0.16, 1, 0.3, 1))
// 迅速启动 + 缓慢刹车,给数字元素物理重量感
expoOut: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
// overshoot: 带弹性的 toggle/按钮弹出 (cubic-bezier(0.34, 1.56, 0.64, 1))
overshoot: t => {
const c1 = 1.70158, c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
},
spring: t => {
const c = (2 * Math.PI) / 3;
return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c) + 1;
},
anticipation: t => {
if (t < 0.2) return -0.3 * (t / 0.2) * (t / 0.2);
const adjusted = (t - 0.2) / 0.8;
return -0.012 + 1.012 * adjusted * adjusted * (3 - 2 * adjusted);
},
};
function interpolate(t, input, output, easing) {
const [inStart, inEnd] = input;
const [outStart, outEnd] = output;
if (t <= inStart) return outStart;
if (t >= inEnd) return outEnd;
let progress = (t - inStart) / (inEnd - inStart);
if (easing) {
progress = easing(progress);
}
return outStart + (outEnd - outStart) * progress;
}
function useTime() {
const ctx = useContext(TimeContext);
return ctx.time;
}
function useSprite() {
const sprite = useContext(SpriteContext);
if (!sprite) {
return { t: 0, elapsed: 0, duration: 0 };
}
return sprite;
}
const stageStyles = {
wrapper: {
position: 'fixed',
inset: 0,
background: '#000',
display: 'flex',
flexDirection: 'column',
fontFamily: '-apple-system, sans-serif',
},
stageHolder: {
flex: 1,
position: 'relative',
overflow: 'hidden',
},
canvas: {
position: 'absolute',
top: '50%',
left: '50%',
transformOrigin: 'center center',
background: '#111',
overflow: 'hidden',
},
controls: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
background: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(10px)',
padding: '12px 20px',
display: 'flex',
alignItems: 'center',
gap: 16,
color: '#fff',
fontSize: 12,
zIndex: 100,
},
button: {
background: 'none',
border: '1px solid rgba(255,255,255,0.3)',
color: '#fff',
padding: '6px 14px',
borderRadius: 4,
cursor: 'pointer',
fontSize: 12,
},
timeDisplay: {
fontFamily: 'ui-monospace, monospace',
fontVariantNumeric: 'tabular-nums',
minWidth: 90,
},
scrubber: {
flex: 1,
height: 4,
background: 'rgba(255,255,255,0.2)',
borderRadius: 2,
position: 'relative',
cursor: 'pointer',
},
scrubberFill: {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
background: '#fff',
borderRadius: 2,
pointerEvents: 'none',
},
scrubberHandle: {
position: 'absolute',
top: '50%',
width: 12,
height: 12,
background: '#fff',
borderRadius: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
},
};
function Stage({ duration = 10, width = 1920, height = 1080, fps = 60, loop = true, children, bgColor = '#fff' }) {
const [time, setTime] = useState(0);
const [playing, setPlaying] = useState(true);
const [scale, setScale] = useState(1);
const rafRef = useRef(null);
const startTimeRef = useRef(performance.now());
const canvasRef = useRef(null);
// Recording mode: render-video.js injects window.__recording = true before goto.
// When set, force loop=false so the export ends on the final frame instead of
// wrapping back to t=0 and capturing the start of the next cycle.
// (Browsers viewing manually still loop because __recording is undefined there.)
const effectiveLoop = (typeof window !== 'undefined' && window.__recording) ? false : loop;
useEffect(() => {
function updateScale() {
const vw = window.innerWidth;
const vh = window.innerHeight - 56;
const s = Math.min(vw / width, vh / height);
setScale(s);
}
updateScale();
window.addEventListener('resize', updateScale);
return () => window.removeEventListener('resize', updateScale);
}, [width, height]);
useEffect(() => {
if (!playing) return;
let cancelled = false;
let last = null;
function tick(now) {
if (cancelled) return;
if (last === null) {
// First animation frame. Set last=now so delta starts at 0,
// AND announce readiness for video export.
// This pairing is critical: window.__ready must flip to true at
// the exact moment WebM captures frame 0 of the animation, so
// render-video.js's trim offset equals the pre-animation gap.
last = now;
if (typeof window !== 'undefined') window.__ready = true;
}
const delta = (now - last) / 1000;
last = now;
setTime(prev => {
const next = prev + delta;
if (next >= duration) {
// effectiveLoop honors window.__recording (forced non-loop during export).
// Stop just shy of duration so the final-frame state stays rendered
// (avoids exiting all Sprites that end exactly at `duration`).
return effectiveLoop ? 0 : duration - 0.001;
}
return next;
});
rafRef.current = requestAnimationFrame(tick);
}
// Wait for fonts before starting the clock — makes frame 0 the
// real "finished-loading" frame users see, not a fallback-font flash.
const startAfterFonts = () => {
if (cancelled) return;
rafRef.current = requestAnimationFrame(tick);
};
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
document.fonts.ready.then(startAfterFonts);
} else {
startAfterFonts();
}
return () => {
cancelled = true;
cancelAnimationFrame(rafRef.current);
};
}, [playing, duration, effectiveLoop]);
const handleScrub = useCallback((e) => {
const rect = e.currentTarget.getBoundingClientRect();
const ratio = (e.clientX - rect.left) / rect.width;
setTime(Math.max(0, Math.min(duration, ratio * duration)));
}, [duration]);
const handleSeek = useCallback((e) => {
handleScrub(e);
setPlaying(false);
}, [handleScrub]);
const progress = time / duration;
const ctx = {
time,
duration,
playing,
setPlaying,
setTime,
};
const canvasStyle = {
...stageStyles.canvas,
width,
height,
background: bgColor,
transform: `translate(-50%, -50%) scale(${scale})`,
};
return (
<TimeContext.Provider value={ctx}>
<div style={stageStyles.wrapper}>
<div style={stageStyles.stageHolder}>
<div ref={canvasRef} style={canvasStyle}>
{children}
</div>
</div>
<div style={stageStyles.controls}>
<button
style={stageStyles.button}
onClick={() => setPlaying(p => !p)}
>
{playing ? '⏸ 暂停' : '▶ 播放'}
</button>
<button
style={stageStyles.button}
onClick={() => setTime(0)}
>
开始
</button>
<div style={stageStyles.timeDisplay}>
{time.toFixed(2)}s / {duration.toFixed(2)}s
</div>
<div style={stageStyles.scrubber} onMouseDown={handleSeek}>
<div style={{ ...stageStyles.scrubberFill, width: `${progress * 100}%` }} />
<div style={{ ...stageStyles.scrubberHandle, left: `${progress * 100}%` }} />
</div>
</div>
</div>
</TimeContext.Provider>
);
}
function Sprite({ start = 0, end, children, style }) {
const { time } = useContext(TimeContext);
const actualEnd = end == null ? Infinity : end;
if (time < start || time >= actualEnd) {
return null;
}
const duration = actualEnd - start;
const elapsed = time - start;
const t = duration === 0 ? 1 : Math.max(0, Math.min(1, elapsed / duration));
const spriteValue = { t, elapsed, duration, start, end: actualEnd };
return (
<SpriteContext.Provider value={spriteValue}>
<div style={{ position: 'absolute', inset: 0, ...style }}>
{children}
</div>
</SpriteContext.Provider>
);
}
if (typeof window !== 'undefined') {
window.Animations = {
Stage,
Sprite,
useTime,
useSprite,
Easing,
interpolate,
};
}
})();

View File

@@ -0,0 +1,138 @@
/* html-ppt :: animations.css
* Apply by adding class="anim-<name>" or data-anim="<name>".
* Durations are deliberately snappy; tweak --anim-dur per element.
*/
:root{--anim-dur:.7s;--anim-ease:cubic-bezier(.4,0,.2,1)}
/* ---------- FADE DIRECTIONALS ---------- */
@keyframes kf-fade-up{from{opacity:0;transform:translateY(32px)}to{opacity:1;transform:none}}
@keyframes kf-fade-down{from{opacity:0;transform:translateY(-32px)}to{opacity:1;transform:none}}
@keyframes kf-fade-left{from{opacity:0;transform:translateX(-40px)}to{opacity:1;transform:none}}
@keyframes kf-fade-right{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
.anim-fade-up{animation:kf-fade-up var(--anim-dur) var(--anim-ease) both}
.anim-fade-down{animation:kf-fade-down var(--anim-dur) var(--anim-ease) both}
.anim-fade-left{animation:kf-fade-left var(--anim-dur) var(--anim-ease) both}
.anim-fade-right{animation:kf-fade-right var(--anim-dur) var(--anim-ease) both}
/* ---------- RISE / DROP / ZOOM / BLUR / GLITCH ---------- */
@keyframes kf-rise{from{opacity:0;transform:translateY(60px) scale(.97);filter:blur(6px)}to{opacity:1;transform:none;filter:none}}
@keyframes kf-drop{from{opacity:0;transform:translateY(-60px) scale(.97)}to{opacity:1;transform:none}}
@keyframes kf-zoom{0%{opacity:0;transform:scale(.6)}60%{transform:scale(1.04)}100%{opacity:1;transform:scale(1)}}
@keyframes kf-blur{from{opacity:0;filter:blur(18px)}to{opacity:1;filter:none}}
@keyframes kf-glitch{0%{opacity:0;transform:translateX(0);clip-path:inset(0 0 0 0)}
20%{opacity:1;transform:translateX(-6px);clip-path:inset(20% 0 30% 0)}
40%{transform:translateX(4px);clip-path:inset(50% 0 10% 0)}
60%{transform:translateX(-3px);clip-path:inset(10% 0 60% 0)}
80%{transform:translateX(2px);clip-path:inset(0 0 0 0)}
100%{opacity:1;transform:none}}
.anim-rise-in{animation:kf-rise .9s var(--anim-ease) both}
.anim-drop-in{animation:kf-drop .8s var(--anim-ease) both}
.anim-zoom-pop{animation:kf-zoom .7s cubic-bezier(.22,1.3,.36,1) both}
.anim-blur-in{animation:kf-blur .8s var(--anim-ease) both}
.anim-glitch-in{animation:kf-glitch .8s steps(5,end) both}
/* ---------- TYPEWRITER ---------- */
.anim-typewriter{display:inline-block;overflow:hidden;white-space:nowrap;border-right:2px solid currentColor;
width:0;animation:kf-type 2.4s steps(40,end) forwards, kf-caret 1s step-end infinite}
@keyframes kf-type{to{width:100%}}
@keyframes kf-caret{50%{border-color:transparent}}
/* ---------- GLOW / SHIMMER / GRADIENT-FLOW ---------- */
@keyframes kf-neon{0%,100%{text-shadow:0 0 8px var(--accent),0 0 20px var(--accent)}
50%{text-shadow:0 0 16px var(--accent),0 0 40px var(--accent),0 0 80px var(--accent)}}
.anim-neon-glow{animation:kf-neon 2s ease-in-out infinite}
.anim-shimmer-sweep{position:relative;overflow:hidden}
.anim-shimmer-sweep::after{content:"";position:absolute;inset:0;
background:linear-gradient(110deg,transparent 40%,rgba(255,255,255,.55) 50%,transparent 60%);
transform:translateX(-100%);animation:kf-shimmer 2.4s var(--anim-ease) infinite}
@keyframes kf-shimmer{to{transform:translateX(100%)}}
.anim-gradient-flow{background:linear-gradient(90deg,var(--accent),var(--accent-2,var(--accent)),var(--accent-3,var(--accent)),var(--accent));
background-size:300% 100%;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;
animation:kf-gradflow 4s linear infinite}
@keyframes kf-gradflow{to{background-position:300% 0}}
/* ---------- STAGGER LIST ---------- */
.anim-stagger-list > *{opacity:0;animation:kf-rise .65s var(--anim-ease) both}
.anim-stagger-list > *:nth-child(1){animation-delay:.05s}
.anim-stagger-list > *:nth-child(2){animation-delay:.15s}
.anim-stagger-list > *:nth-child(3){animation-delay:.25s}
.anim-stagger-list > *:nth-child(4){animation-delay:.35s}
.anim-stagger-list > *:nth-child(5){animation-delay:.45s}
.anim-stagger-list > *:nth-child(6){animation-delay:.55s}
.anim-stagger-list > *:nth-child(7){animation-delay:.65s}
.anim-stagger-list > *:nth-child(8){animation-delay:.75s}
.anim-stagger-list > *:nth-child(n+9){animation-delay:.85s}
/* ---------- COUNTER-UP (JS-driven, marker class only) ---------- */
.counter{font-variant-numeric:tabular-nums}
/* ---------- SVG PATH DRAW ---------- */
.anim-path-draw path,.anim-path-draw line,.anim-path-draw polyline,.anim-path-draw circle,.anim-path-draw rect{
stroke-dasharray:1000;stroke-dashoffset:1000;animation:kf-draw 2s var(--anim-ease) forwards}
@keyframes kf-draw{to{stroke-dashoffset:0}}
/* ---------- PARALLAX TILT (hover) ---------- */
.anim-parallax-tilt{transform-style:preserve-3d;transition:transform .4s var(--anim-ease)}
.anim-parallax-tilt:hover{transform:perspective(900px) rotateX(6deg) rotateY(-8deg) translateZ(10px)}
/* ---------- CARD FLIP 3D ---------- */
@keyframes kf-flip{from{transform:perspective(1200px) rotateY(-90deg);opacity:0}
to{transform:perspective(1200px) rotateY(0);opacity:1}}
.anim-card-flip-3d{animation:kf-flip .9s var(--anim-ease) both;transform-style:preserve-3d;backface-visibility:hidden}
/* ---------- CUBE ROTATE 3D ---------- */
@keyframes kf-cube{from{transform:perspective(1200px) rotateX(20deg) rotateY(-90deg) translateZ(-200px);opacity:0}
to{transform:perspective(1200px) rotateX(0) rotateY(0) translateZ(0);opacity:1}}
.anim-cube-rotate-3d{animation:kf-cube 1s var(--anim-ease) both}
/* ---------- PAGE TURN 3D ---------- */
@keyframes kf-pageturn{from{transform:perspective(1600px) rotateY(-85deg);transform-origin:left center;opacity:0}
to{transform:perspective(1600px) rotateY(0);opacity:1}}
.anim-page-turn-3d{animation:kf-pageturn 1s var(--anim-ease) both;transform-origin:left center}
/* ---------- PERSPECTIVE ZOOM ---------- */
@keyframes kf-pzoom{from{opacity:0;transform:perspective(1400px) translateZ(-400px) rotateX(12deg)}
to{opacity:1;transform:none}}
.anim-perspective-zoom{animation:kf-pzoom 1s var(--anim-ease) both}
/* ---------- MARQUEE SCROLL ---------- */
.anim-marquee-scroll{display:flex;gap:48px;white-space:nowrap;animation:kf-marquee 20s linear infinite}
@keyframes kf-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
/* ---------- KEN BURNS ---------- */
@keyframes kf-kenburns{0%{transform:scale(1) translate(0,0)}100%{transform:scale(1.15) translate(-2%,-1%)}}
.anim-kenburns{animation:kf-kenburns 14s ease-in-out infinite alternate}
/* ---------- CONFETTI BURST (pseudo — pure CSS sparkles) ---------- */
.anim-confetti-burst{position:relative}
.anim-confetti-burst::before,.anim-confetti-burst::after{
content:"";position:absolute;top:50%;left:50%;width:8px;height:8px;border-radius:50%;
background:var(--accent);box-shadow:
20px -30px 0 var(--accent-2,var(--accent)),-25px -20px 0 var(--accent-3,var(--accent)),
30px 20px 0 var(--good,#1aaf6c),-30px 25px 0 var(--warn,#f5a524),
40px -10px 0 var(--bad,#e0445a),-45px 0 0 var(--accent),
10px 40px 0 var(--accent-2,var(--accent)),-15px -40px 0 var(--accent-3,var(--accent));
opacity:0;animation:kf-confetti 1.2s var(--anim-ease) forwards}
.anim-confetti-burst::after{animation-delay:.15s;transform:rotate(45deg)}
@keyframes kf-confetti{0%{opacity:0;transform:scale(.2)}30%{opacity:1}100%{opacity:0;transform:scale(2.2)}}
/* ---------- SPOTLIGHT ---------- */
@keyframes kf-spot{0%{clip-path:circle(0% at 50% 50%)}100%{clip-path:circle(140% at 50% 50%)}}
.anim-spotlight{animation:kf-spot 1.1s var(--anim-ease) both}
/* ---------- MORPH SHAPE (SVG) ---------- */
.anim-morph-shape path{animation:kf-morph 6s ease-in-out infinite alternate}
@keyframes kf-morph{0%{d:path("M60,120 Q120,20 180,120 T300,120")}
100%{d:path("M60,120 Q120,220 180,120 T300,120")}}
/* ---------- RIPPLE REVEAL ---------- */
@keyframes kf-ripple{0%{clip-path:circle(0% at 20% 80%);opacity:.4}
100%{clip-path:circle(160% at 20% 80%);opacity:1}}
.anim-ripple-reveal{animation:kf-ripple 1.2s var(--anim-ease) both}
/* reduced motion */
@media (prefers-reduced-motion: reduce){
[class*="anim-"]{animation:none!important;transition:none!important}
}

View File

@@ -0,0 +1,99 @@
/* html-ppt :: fx-runtime.js
* Canvas FX autoloader + lifecycle manager.
* - Dynamically loads all fx modules listed in FX_LIST
* - Initializes [data-fx] elements when their slide becomes active
* - Calls handle.stop() when the slide leaves
*/
(function(){
'use strict';
const FX_LIST = [
'_util',
'particle-burst','confetti-cannon','firework','starfield','matrix-rain',
'knowledge-graph','neural-net','constellation','orbit-ring','galaxy-swirl',
'word-cascade','letter-explode','chain-react','magnetic-field','data-stream',
'gradient-blob','sparkle-trail','shockwave','typewriter-multi','counter-explosion'
];
// Resolve base path of this script so it works from any page location.
const myScript = document.currentScript || (function(){
const all = document.getElementsByTagName('script');
for (const s of all){ if (s.src && s.src.indexOf('fx-runtime.js')>-1) return s; }
return null;
})();
const base = myScript ? myScript.src.replace(/fx-runtime\.js.*$/, 'fx/') : 'assets/animations/fx/';
let loaded = 0;
const total = FX_LIST.length;
const ready = new Promise((resolve) => {
if (!total) return resolve();
FX_LIST.forEach((name) => {
const s = document.createElement('script');
s.src = base + name + '.js';
s.async = false;
s.onload = s.onerror = () => { if (++loaded >= total) resolve(); };
document.head.appendChild(s);
});
});
window.__hpxActive = window.__hpxActive || new Map();
function initFxIn(root){
if (!window.HPX) return;
const els = root.querySelectorAll('[data-fx]');
els.forEach((el) => {
if (window.__hpxActive.has(el)) return;
const name = el.getAttribute('data-fx');
const fn = window.HPX[name];
if (typeof fn !== 'function') return;
try {
const handle = fn(el, {}) || { stop(){} };
window.__hpxActive.set(el, handle);
} catch(e){ console.warn('[hpx-fx]', name, e); }
});
}
function stopFxIn(root){
const els = root.querySelectorAll('[data-fx]');
els.forEach((el) => {
const h = window.__hpxActive.get(el);
if (h && typeof h.stop === 'function'){
try{ h.stop(); }catch(e){}
}
window.__hpxActive.delete(el);
});
}
function reinitFxIn(root){
stopFxIn(root);
initFxIn(root);
}
window.__hpxReinit = reinitFxIn;
function boot(){
ready.then(() => {
const active = document.querySelector('.slide.is-active') || document.querySelector('.slide');
if (active) initFxIn(active);
// Watch all slides for class changes
const slides = document.querySelectorAll('.slide');
slides.forEach((sl) => {
const mo = new MutationObserver((muts) => {
for (const m of muts){
if (m.attributeName === 'class'){
if (sl.classList.contains('is-active')) initFxIn(sl);
else stopFxIn(sl);
}
}
});
mo.observe(sl, { attributes: true, attributeFilter: ['class'] });
});
});
}
if (document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();

View File

@@ -0,0 +1,63 @@
/* html-ppt fx :: shared helpers */
(function(){
window.HPX = window.HPX || {};
const U = window.HPX._u = {};
U.css = (el, name, fb) => {
const v = getComputedStyle(el).getPropertyValue(name).trim();
return v || fb;
};
U.accent = (el, fb) => U.css(el, '--accent', fb || '#7c5cff');
U.accent2 = (el, fb) => U.css(el, '--accent-2', fb || '#22d3ee');
U.text = (el, fb) => U.css(el, '--text-1', fb || '#eaeaf2');
U.palette = (el) => [
U.accent(el, '#7c5cff'),
U.accent2(el, '#22d3ee'),
U.css(el, '--ok', '#22c55e'),
U.css(el, '--warn', '#f59e0b'),
U.css(el, '--danger', '#ef4444'),
];
U.canvas = (el) => {
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const c = document.createElement('canvas');
c.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;display:block;';
el.appendChild(c);
const ctx = c.getContext('2d');
let w = 0, h = 0, dpr = Math.max(1, Math.min(2, window.devicePixelRatio||1));
const fit = () => {
const r = el.getBoundingClientRect();
w = Math.max(1, r.width|0);
h = Math.max(1, r.height|0);
c.width = (w*dpr)|0;
c.height = (h*dpr)|0;
ctx.setTransform(dpr,0,0,dpr,0,0);
};
fit();
const ro = new ResizeObserver(fit);
ro.observe(el);
return {
c, ctx,
get w(){return w;}, get h(){return h;}, get dpr(){return dpr;},
destroy(){
try{ro.disconnect();}catch(e){}
if (c.parentNode) c.parentNode.removeChild(c);
}
};
};
U.loop = (fn) => {
let raf = 0, stopped = false, t0 = performance.now();
const tick = (t) => {
if (stopped) return;
fn((t - t0)/1000);
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => { stopped = true; cancelAnimationFrame(raf); };
};
U.rand = (a,b) => a + Math.random()*(b-a);
})();

View File

@@ -0,0 +1,41 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['chain-react'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
const N = 8;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
const cy = k.h/2;
const pad = 60;
const dx = (k.w - pad*2)/(N-1);
const period = 2.4;
const phase = (t % period) / period; // 0..1
for (let i=0;i<N;i++){
const x = pad + i*dx;
const my = i/(N-1);
const d = Math.abs(phase - my);
const pulse = Math.max(0, 1 - d*6);
const r = 18 + pulse*18;
// glow
const g = ctx.createRadialGradient(x,cy,0,x,cy,r*2);
g.addColorStop(0, `rgba(124,92,255,${0.4*pulse})`);
g.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = g;
ctx.fillRect(x-r*2, cy-r*2, r*4, r*4);
// circle
ctx.fillStyle = pulse>0.1 ? ac2 : ac;
ctx.beginPath(); ctx.arc(x,cy,r,0,Math.PI*2); ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.4)'; ctx.lineWidth=2;
ctx.stroke();
// connectors
if (i<N-1){
ctx.strokeStyle='rgba(200,200,230,0.3)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x+r,cy); ctx.lineTo(x+dx-r,cy); ctx.stroke();
}
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,49 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['confetti-cannon'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let parts = [];
const fire = () => {
for (let side=0; side<2; side++){
const x0 = side===0 ? 20 : k.w-20;
const y0 = k.h - 20;
for (let i=0;i<40;i++){
const a = side===0 ? U.rand(-Math.PI*0.7, -Math.PI*0.4) : U.rand(-Math.PI*0.6, -Math.PI*0.3) - Math.PI/2 - Math.PI/6;
const spd = U.rand(300, 520);
parts.push({
x: x0, y: y0,
vx: Math.cos(a)*spd, vy: Math.sin(a)*spd,
w: U.rand(6,12), h: U.rand(3,7),
rot: Math.random()*Math.PI, vr: U.rand(-6,6),
c: pal[(Math.random()*pal.length)|0],
life: 1
});
}
}
};
fire();
let last = 0;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - last > 3) { fire(); last = t; }
const dt = 1/60;
parts = parts.filter(p => p.life > 0 && p.y < k.h+40);
for (const p of parts){
p.vy += 520*dt;
p.x += p.vx*dt; p.y += p.vy*dt;
p.rot += p.vr*dt;
p.life -= 0.006;
ctx.save();
ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.fillRect(-p.w/2, -p.h/2, p.w, p.h);
ctx.restore();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,44 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['constellation'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#9fb4ff');
const N = 70;
let pts = [];
const seed = () => {
pts = Array.from({length:N}, () => ({
x: Math.random()*k.w, y: Math.random()*k.h,
vx: U.rand(-0.3,0.3), vy: U.rand(-0.3,0.3)
}));
};
seed();
let lw=k.w, lh=k.h;
const stop = U.loop(() => {
if (k.w!==lw||k.h!==lh){ seed(); lw=k.w; lh=k.h; }
ctx.clearRect(0,0,k.w,k.h);
for (const p of pts){
p.x += p.vx; p.y += p.vy;
if (p.x<0||p.x>k.w) p.vx*=-1;
if (p.y<0||p.y>k.h) p.vy*=-1;
}
for (let i=0;i<N;i++){
for (let j=i+1;j<N;j++){
const a=pts[i], b=pts[j];
const d = Math.hypot(a.x-b.x, a.y-b.y);
if (d < 150){
ctx.globalAlpha = 1 - d/150;
ctx.strokeStyle = ac; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
}
}
ctx.globalAlpha = 1;
ctx.fillStyle = ac;
for (const p of pts){
ctx.beginPath(); ctx.arc(p.x,p.y,1.8,0,Math.PI*2); ctx.fill();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,58 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['counter-explosion'] = function(el){
const U = window.HPX._u;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const target = parseInt(el.getAttribute('data-fx-to') || '2400', 10);
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
// number overlay
const num = document.createElement('div');
num.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font:900 120px system-ui,sans-serif;color:var(--text-1,#fff);pointer-events:none;text-shadow:0 4px 40px rgba(124,92,255,0.5);';
num.textContent = '0';
el.appendChild(num);
let parts = [];
let state = 'count'; // count | burst | hold
let stateT = 0;
let value = 0;
let cycle = 0;
const burst = () => {
const cx = k.w/2, cy = k.h/2;
for (let i=0;i<120;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(120, 400);
parts.push({x:cx,y:cy,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,r:U.rand(2,5),c:pal[(Math.random()*pal.length)|0]});
}
};
const stop = U.loop(() => {
ctx.clearRect(0,0,k.w,k.h);
const dt = 1/60;
stateT += dt;
if (state === 'count'){
const dur = 2.2;
const p = Math.min(1, stateT/dur);
const eased = 1 - Math.pow(1-p,3);
value = Math.round(target*eased);
num.textContent = value.toLocaleString();
if (p >= 1){ state='burst'; stateT=0; burst(); }
} else if (state === 'burst'){
if (stateT > 0.05 && stateT < 0.3 && parts.length < 200) {}
if (stateT > 2.5){ state='hold'; stateT=0; }
} else if (state === 'hold'){
if (stateT > 1.5){
state='count'; stateT=0; value=0; num.textContent='0'; cycle++;
}
}
parts = parts.filter(p => p.life > 0);
for (const p of parts){
p.vy += 260*dt; p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx*dt; p.y += p.vy*dt; p.life -= 0.01;
ctx.globalAlpha = Math.max(0,p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); if (num.parentNode) num.parentNode.removeChild(num); } };
};
})();

View File

@@ -0,0 +1,45 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['data-stream'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#22d3ee'), ac2 = U.accent2(el,'#7c5cff');
const rows = [];
const rh = 22;
const genRow = (y) => ({
y, dir: Math.random()<0.5?-1:1,
speed: U.rand(30, 90),
offset: Math.random()*2000,
text: Array.from({length:120}, () => {
const r = Math.random();
if (r<0.3) return Math.random()<0.5?'0':'1';
if (r<0.6) return '0x' + Math.floor(Math.random()*256).toString(16).padStart(2,'0');
return Math.random().toString(16).slice(2,6);
}).join(' ')
});
const init = () => {
rows.length = 0;
const n = Math.ceil(k.h/rh);
for (let i=0;i<n;i++) rows.push(genRow(i*rh + rh*0.7));
};
init();
let lh = k.h;
const stop = U.loop((t) => {
if (k.h!==lh){ init(); lh=k.h; }
ctx.fillStyle = 'rgba(5,8,14,0.35)';
ctx.fillRect(0,0,k.w,k.h);
ctx.font = '13px ui-monospace,Menlo,monospace';
for (let i=0;i<rows.length;i++){
const r = rows[i];
const x = r.dir>0
? ((t*r.speed + r.offset) % (k.w+400)) - 400
: k.w - (((t*r.speed + r.offset) % (k.w+400)) - 400);
ctx.fillStyle = (i%3===0)?ac:ac2;
ctx.globalAlpha = 0.65 + (i%2)*0.3;
ctx.fillText(r.text, x, r.y);
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,51 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['firework'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let rockets = [], sparks = [];
const launch = () => {
rockets.push({
x: U.rand(k.w*0.2, k.w*0.8), y: k.h+10,
vx: U.rand(-30,30), vy: U.rand(-520,-380),
tgtY: U.rand(k.h*0.15, k.h*0.45),
c: pal[(Math.random()*pal.length)|0]
});
};
const burst = (x, y, c) => {
const n = 70;
for (let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(60, 240);
sparks.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,c});
}
};
let last = -1;
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.18)';
ctx.fillRect(0,0,k.w,k.h);
if (t - last > 0.7) { launch(); last = t; }
const dt = 1/60;
rockets = rockets.filter(r => {
r.x += r.vx*dt; r.y += r.vy*dt; r.vy += 260*dt;
ctx.fillStyle = r.c;
ctx.beginPath(); ctx.arc(r.x, r.y, 2.5, 0, Math.PI*2); ctx.fill();
if (r.y <= r.tgtY || r.vy >= 0) { burst(r.x, r.y, r.c); return false; }
return true;
});
sparks = sparks.filter(p => p.life > 0);
for (const p of sparks){
p.vy += 90*dt;
p.vx *= 0.98; p.vy *= 0.98;
p.x += p.vx*dt; p.y += p.vy*dt;
p.life -= 0.012;
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,33 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['galaxy-swirl'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const N = 800;
const parts = Array.from({length:N}, (_,i) => {
const arm = i%3;
const t = Math.random();
const r = t*180 + 8;
const base = (arm/3)*Math.PI*2;
return { r, a: base + Math.log(r+1)*1.6 + U.rand(-0.2,0.2),
c: pal[arm%pal.length],
s: U.rand(0.8, 2.2) };
});
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0,0,k.w,k.h);
const cx=k.w/2, cy=k.h/2;
for (const p of parts){
const a = p.a + t*0.15;
const x = cx + Math.cos(a)*p.r;
const y = cy + Math.sin(a)*p.r*0.7;
ctx.fillStyle = p.c;
ctx.globalAlpha = 0.7;
ctx.beginPath(); ctx.arc(x,y,p.s,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,39 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['gradient-blob'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const blobs = Array.from({length:4}, (_,i) => ({
x: U.rand(0,1), y: U.rand(0,1),
vx: U.rand(-0.08,0.08), vy: U.rand(-0.08,0.08),
r: U.rand(180,320),
c: pal[i%pal.length]
}));
const hex2rgb = (h) => {
const m = h.replace('#','').match(/.{2}/g);
if (!m) return [124,92,255];
return m.map(x=>parseInt(x,16));
};
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(10,12,22,0.2)';
ctx.fillRect(0,0,k.w,k.h);
ctx.globalCompositeOperation = 'lighter';
for (const b of blobs){
b.x += b.vx*0.01; b.y += b.vy*0.01;
if (b.x<0||b.x>1) b.vx*=-1;
if (b.y<0||b.y>1) b.vy*=-1;
const px = b.x*k.w, py = b.y*k.h;
const r = b.r + Math.sin(t*0.8 + b.x*6)*30;
const [R,G,B] = hex2rgb(b.c);
const grad = ctx.createRadialGradient(px,py,0,px,py,r);
grad.addColorStop(0, `rgba(${R},${G},${B},0.55)`);
grad.addColorStop(1, `rgba(${R},${G},${B},0)`);
ctx.fillStyle = grad;
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,69 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['knowledge-graph'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const tx = U.text(el, '#e7e7ef');
const labels = ['AI','ML','LLM','Graph','Node','Edge','Claude','GPT','RAG','Vector',
'Embed','Neural','Agent','Tool','Memory','Logic','Data','Train','Infer','Token',
'Prompt','Chain','Plan','Skill','Cloud','Edge','GPU','Code','Task','Flow'];
const N = 28;
const nodes = Array.from({length:N}, (_,i) => ({
x: U.rand(40, 300), y: U.rand(40, 200),
vx: 0, vy: 0, label: labels[i%labels.length],
c: pal[i%pal.length]
}));
const edges = [];
const made = new Set();
while (edges.length < 50){
const a = (Math.random()*N)|0, b = (Math.random()*N)|0;
if (a===b) continue;
const key = a<b ? a+'-'+b : b+'-'+a;
if (made.has(key)) continue;
made.add(key); edges.push([a,b]);
}
const stop = U.loop(() => {
// physics
for (let i=0;i<N;i++){
for (let j=i+1;j<N;j++){
const a=nodes[i], b=nodes[j];
const dx=b.x-a.x, dy=b.y-a.y;
let d2=dx*dx+dy*dy; if (d2<1) d2=1;
const d=Math.sqrt(d2);
const f=1600/d2;
const fx=(dx/d)*f, fy=(dy/d)*f;
a.vx-=fx; a.vy-=fy; b.vx+=fx; b.vy+=fy;
}
}
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
const dx=b.x-a.x, dy=b.y-a.y, d=Math.hypot(dx,dy)||1;
const f=(d-90)*0.008;
const fx=(dx/d)*f, fy=(dy/d)*f;
a.vx+=fx; a.vy+=fy; b.vx-=fx; b.vy-=fy;
}
const cx=k.w/2, cy=k.h/2;
for (const n of nodes){
n.vx += (cx-n.x)*0.002;
n.vy += (cy-n.y)*0.002;
n.vx *= 0.85; n.vy *= 0.85;
n.x += n.vx; n.y += n.vy;
}
ctx.clearRect(0,0,k.w,k.h);
ctx.strokeStyle = 'rgba(180,180,220,0.25)'; ctx.lineWidth=1;
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
ctx.font='11px system-ui,sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
for (const n of nodes){
ctx.fillStyle = n.c;
ctx.beginPath(); ctx.arc(n.x,n.y,7,0,Math.PI*2); ctx.fill();
ctx.fillStyle = tx;
ctx.fillText(n.label, n.x, n.y-14);
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,50 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['letter-explode'] = function(el){
const U = window.HPX._u;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const src = el.querySelector('[data-fx-text]') || el;
const text = (el.getAttribute('data-fx-text-value') || src.textContent || 'EXPLODE').trim();
// Build a container, hide source text
const wrap = document.createElement('div');
wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;';
const inner = document.createElement('div');
inner.style.cssText = 'font-size:64px;font-weight:900;letter-spacing:0.02em;color:var(--text-1,#fff);white-space:nowrap;';
wrap.appendChild(inner);
el.appendChild(wrap);
const spans = [];
for (const ch of text){
const s = document.createElement('span');
s.textContent = ch === ' ' ? '\u00A0' : ch;
s.style.display='inline-block';
s.style.transform='translate(0,0)';
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
s.style.opacity='0';
inner.appendChild(s);
spans.push(s);
}
let stopped = false;
const run = () => {
if (stopped) return;
spans.forEach((s,i) => {
const dx = U.rand(-400, 400), dy = U.rand(-300, 300);
s.style.transition='none';
s.style.transform=`translate(${dx}px,${dy}px) rotate(${U.rand(-180,180)}deg)`;
s.style.opacity='0';
});
// force reflow
void inner.offsetWidth;
spans.forEach((s,i) => {
setTimeout(() => {
if (stopped) return;
s.style.transition='transform 900ms cubic-bezier(.2,.9,.3,1), opacity 900ms';
s.style.transform='translate(0,0) rotate(0deg)';
s.style.opacity='1';
}, i*35);
});
};
run();
const iv = setInterval(run, 4500);
return { stop(){ stopped=true; clearInterval(iv); if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
};
})();

View File

@@ -0,0 +1,40 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['magnetic-field'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const N = 60;
const parts = Array.from({length:N}, (_,i) => ({
phase: Math.random()*Math.PI*2,
freq: U.rand(0.4, 1.2),
amp: U.rand(30, 90),
y0: U.rand(0.15, 0.85),
c: pal[i%pal.length],
trail: []
}));
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(0,0,k.w,k.h);
for (const p of parts){
const x = ((t*80 + p.phase*50) % (k.w+100)) - 50;
const y = k.h*p.y0 + Math.sin(x*0.02 + p.phase + t*p.freq)*p.amp;
p.trail.push([x,y]);
if (p.trail.length > 18) p.trail.shift();
ctx.strokeStyle = p.c;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i=0;i<p.trail.length;i++){
const [tx,ty] = p.trail[i];
if (i===0) ctx.moveTo(tx,ty); else ctx.lineTo(tx,ty);
}
ctx.globalAlpha = 0.7;
ctx.stroke();
ctx.globalAlpha = 1;
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(x,y,2.5,0,Math.PI*2); ctx.fill();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,33 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['matrix-rain'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const glyphs = 'アイウエオカキクケコサシスセソタチツテトナニヌネ0123456789ABCDEF'.split('');
const fs = 16;
let cols = 0, drops = [];
const init = () => {
cols = Math.ceil(k.w/fs);
drops = Array.from({length:cols}, () => U.rand(-20, 0));
};
init();
let lw = k.w, lh = k.h;
const stop = U.loop(() => {
if (k.w!==lw || k.h!==lh){ init(); lw=k.w; lh=k.h; }
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(0,0,k.w,k.h);
ctx.font = fs+'px monospace';
for (let i=0;i<cols;i++){
const ch = glyphs[(Math.random()*glyphs.length)|0];
const x = i*fs, y = drops[i]*fs;
ctx.fillStyle = '#9fffc9';
ctx.fillText(ch, x, y);
ctx.fillStyle = '#00ff6a';
ctx.fillText(ch, x, y - fs);
drops[i] += 1;
if (y > k.h && Math.random() > 0.975) drops[i] = 0;
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,75 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['neural-net'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
const layers = [4,6,6,3];
let nodes = [], edges = [], pulses = [];
const layout = () => {
nodes = [];
const pad = 40;
const cw = k.w - pad*2, ch = k.h - pad*2;
for (let L=0; L<layers.length; L++){
const x = pad + (cw * L / (layers.length-1));
const n = layers[L];
for (let i=0;i<n;i++){
const y = pad + (ch * (i+0.5) / n);
nodes.push({x,y,L,i});
}
}
edges = [];
for (let L=0; L<layers.length-1; L++){
const a = nodes.filter(n=>n.L===L), b = nodes.filter(n=>n.L===L+1);
for (const x of a) for (const y of b) edges.push([nodes.indexOf(x),nodes.indexOf(y)]);
}
};
layout();
let lw=k.w, lh=k.h, last=0;
const stop = U.loop((t) => {
if (k.w!==lw||k.h!==lh){ layout(); lw=k.w; lh=k.h; }
ctx.clearRect(0,0,k.w,k.h);
ctx.strokeStyle = 'rgba(160,160,200,0.22)'; ctx.lineWidth=1;
for (const [i,j] of edges){
const a=nodes[i], b=nodes[j];
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
}
if (t - last > 0.25){
last = t;
const starts = nodes.filter(n=>n.L===0);
const s = starts[(Math.random()*starts.length)|0];
pulses.push({node:s, L:0, t:0});
}
pulses = pulses.filter(p => p.L < layers.length-1);
for (const p of pulses){
p.t += 0.03;
if (p.t >= 1){
const next = nodes.filter(n=>n.L===p.L+1);
p.node2 = next[(Math.random()*next.length)|0];
if (!p._started){ p._started = true; }
}
}
// animate progression
for (const p of pulses){
if (!p.target){
const next = nodes.filter(n=>n.L===p.L+1);
p.target = next[(Math.random()*next.length)|0];
}
p.t += 0.04;
const a = p.node, b = p.target;
const x = a.x + (b.x-a.x)*Math.min(1,p.t);
const y = a.y + (b.y-a.y)*Math.min(1,p.t);
ctx.fillStyle = ac2;
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
if (p.t >= 1){ p.node = b; p.target=null; p.L++; p.t=0; }
}
for (const n of nodes){
ctx.fillStyle = ac;
ctx.beginPath(); ctx.arc(n.x,n.y,6,0,Math.PI*2); ctx.fill();
ctx.strokeStyle = ac2; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.arc(n.x,n.y,8,0,Math.PI*2); ctx.stroke();
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,38 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['orbit-ring'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const rings = [
{r:40, n:3, sp:1.2, c:pal[0]},
{r:75, n:5, sp:0.8, c:pal[1]},
{r:110, n:8, sp:-0.6, c:pal[2]},
{r:145, n:12, sp:0.4, c:pal[3]},
{r:180, n:16, sp:-0.3, c:pal[4]}
];
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
const cx=k.w/2, cy=k.h/2;
// radial glow
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,210);
g.addColorStop(0,'rgba(124,92,255,0.25)');
g.addColorStop(1,'rgba(0,0,0,0)');
ctx.fillStyle = g; ctx.fillRect(0,0,k.w,k.h);
for (const R of rings){
ctx.strokeStyle = 'rgba(200,200,230,0.2)'; ctx.lineWidth=1;
ctx.beginPath(); ctx.arc(cx,cy,R.r,0,Math.PI*2); ctx.stroke();
for (let i=0;i<R.n;i++){
const a = (i/R.n)*Math.PI*2 + t*R.sp;
const x = cx + Math.cos(a)*R.r;
const y = cy + Math.sin(a)*R.r;
ctx.fillStyle = R.c;
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
}
}
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.fill();
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,42 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['particle-burst'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
let parts = [];
const spawn = () => {
const cx = k.w/2, cy = k.h/2;
const n = 90;
for (let i=0;i<n;i++){
const a = Math.random()*Math.PI*2;
const s = U.rand(80, 260);
parts.push({
x: cx, y: cy,
vx: Math.cos(a)*s, vy: Math.sin(a)*s,
life: 1, r: U.rand(2,5),
c: pal[(Math.random()*pal.length)|0]
});
}
};
spawn();
let lastSpawn = 0;
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - lastSpawn > 2.5) { spawn(); lastSpawn = t; }
const dt = 1/60;
parts = parts.filter(p => p.life > 0);
for (const p of parts){
p.vy += 220*dt;
p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx*dt; p.y += p.vy*dt;
p.life -= 0.012;
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,39 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['shockwave'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const ac = U.accent(el,'#7c5cff'), ac2 = U.accent2(el,'#22d3ee');
let waves = [];
let last = -1;
const stop = U.loop((t) => {
ctx.fillStyle = 'rgba(0,0,0,0.12)';
ctx.fillRect(0,0,k.w,k.h);
if (t - last > 0.6){ last = t; waves.push({t:0}); }
const cx=k.w/2, cy=k.h/2;
const max = Math.hypot(k.w,k.h)/2;
waves = waves.filter(w => w.t < 1);
for (const w of waves){
w.t += 0.012;
const r = w.t * max;
const alpha = 1 - w.t;
ctx.strokeStyle = w.t<0.5?ac2:ac;
ctx.globalAlpha = alpha;
ctx.lineWidth = 3 + (1-w.t)*3;
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.globalAlpha = alpha*0.4;
ctx.beginPath(); ctx.arc(cx,cy,r*0.92,0,Math.PI*2); ctx.stroke();
}
ctx.globalAlpha = 1;
// core
const g = ctx.createRadialGradient(cx,cy,0,cx,cy,40);
g.addColorStop(0,'rgba(255,255,255,0.9)');
g.addColorStop(1,'rgba(124,92,255,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(cx,cy,40,0,Math.PI*2); ctx.fill();
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,62 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['sparkle-trail'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
k.c.style.pointerEvents = 'none';
el.style.cursor = 'crosshair';
const pal = U.palette(el);
let sparks = [];
const onMove = (e) => {
const r = el.getBoundingClientRect();
const x = e.clientX - r.left, y = e.clientY - r.top;
for (let i=0;i<3;i++){
sparks.push({
x, y,
vx: U.rand(-60,60), vy: U.rand(-80,20),
life: 1, c: pal[(Math.random()*pal.length)|0],
r: U.rand(1.5,3.5)
});
}
};
// auto-wiggle if no mouse moves
let auto = true, autoT = 0;
const onAny = () => { auto = false; };
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerenter', onAny);
const stop = U.loop(() => {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0,0,k.w,k.h);
if (auto){
autoT += 0.04;
const x = k.w/2 + Math.cos(autoT)*k.w*0.3;
const y = k.h/2 + Math.sin(autoT*1.3)*k.h*0.3;
for (let i=0;i<3;i++){
sparks.push({
x, y,
vx: U.rand(-60,60), vy: U.rand(-80,20),
life: 1, c: pal[(Math.random()*pal.length)|0],
r: U.rand(1.5,3.5)
});
}
}
const dt = 1/60;
sparks = sparks.filter(s => s.life > 0);
for (const s of sparks){
s.vy += 160*dt;
s.x += s.vx*dt; s.y += s.vy*dt;
s.life -= 0.018;
ctx.globalAlpha = Math.max(0, s.life);
ctx.fillStyle = s.c;
ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){
el.removeEventListener('pointermove', onMove);
el.removeEventListener('pointerenter', onAny);
el.style.cursor = '';
stop(); k.destroy();
}};
};
})();

View File

@@ -0,0 +1,30 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['starfield'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const tx = U.text(el, '#ffffff');
const N = 260;
const stars = Array.from({length:N}, () => ({
x: U.rand(-1,1), y: U.rand(-1,1), z: Math.random()
}));
const stop = U.loop(() => {
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillRect(0,0,k.w,k.h);
const cx = k.w/2, cy = k.h/2;
for (const s of stars){
s.z -= 0.006;
if (s.z <= 0.02) { s.x = U.rand(-1,1); s.y = U.rand(-1,1); s.z = 1; }
const px = cx + (s.x/s.z)*cx;
const py = cy + (s.y/s.z)*cy;
if (px<0||py<0||px>k.w||py>k.h) continue;
const r = (1-s.z)*2.4;
ctx.globalAlpha = 1-s.z;
ctx.fillStyle = tx;
ctx.beginPath(); ctx.arc(px,py,r,0,Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
});
return { stop(){ stop(); k.destroy(); } };
};
})();

View File

@@ -0,0 +1,51 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['typewriter-multi'] = function(el){
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
const lines = [
(el.getAttribute('data-fx-line1') || '> initializing knowledge graph...'),
(el.getAttribute('data-fx-line2') || '> loading 28 concept nodes'),
(el.getAttribute('data-fx-line3') || '> agent ready. awaiting prompt_'),
];
const wrap = document.createElement('div');
wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;gap:14px;padding:32px 48px;font:600 22px ui-monospace,Menlo,monospace;color:var(--text-1,#e7e7ef);';
el.appendChild(wrap);
const rows = lines.map((txt) => {
const row = document.createElement('div');
row.style.cssText = 'white-space:pre;display:flex;align-items:center;';
const span = document.createElement('span'); span.textContent = '';
const cur = document.createElement('span');
cur.textContent = '\u2588';
cur.style.cssText = 'display:inline-block;margin-left:2px;color:var(--accent,#22d3ee);animation:hpxBlink 1s steps(2) infinite;';
row.appendChild(span); row.appendChild(cur);
wrap.appendChild(row);
return {row, span, txt, i:0};
});
// inject blink keyframes once
if (!document.getElementById('hpx-blink-kf')){
const st = document.createElement('style');
st.id = 'hpx-blink-kf';
st.textContent = '@keyframes hpxBlink{50%{opacity:0}}';
document.head.appendChild(st);
}
let stopped = false;
const speeds = [55, 70, 45];
rows.forEach((r, idx) => {
const tick = () => {
if (stopped) return;
if (r.i < r.txt.length){
r.span.textContent += r.txt[r.i++];
setTimeout(tick, speeds[idx]);
} else {
setTimeout(() => {
if (stopped) return;
r.i = 0; r.span.textContent = '';
tick();
}, 2200);
}
};
setTimeout(tick, idx*400);
});
return { stop(){ stopped = true; if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } };
};
})();

View File

@@ -0,0 +1,47 @@
(function(){
window.HPX = window.HPX || {};
window.HPX['word-cascade'] = function(el){
const U = window.HPX._u;
const k = U.canvas(el), ctx = k.ctx;
const pal = U.palette(el);
const WORDS = ['AI','知识','Graph','Claude','LLM','Agent','Vector','RAG','Token','神经',
'Prompt','Chain','Skill','Code','Cloud','GPU','Flow','推理','Data','Model'];
let items = [];
let last = -1;
let piles = {}; // column -> stack height
const stop = U.loop((t) => {
ctx.clearRect(0,0,k.w,k.h);
if (t - last > 0.18){
last = t;
const w = WORDS[(Math.random()*WORDS.length)|0];
items.push({
text: w, x: U.rand(40, k.w-40), y: -20,
vy: 0, c: pal[(Math.random()*pal.length)|0],
size: U.rand(16,26), landed: false
});
}
ctx.textAlign='center'; ctx.textBaseline='middle';
for (const it of items){
if (!it.landed){
it.vy += 0.4;
it.y += it.vy;
const col = Math.round(it.x/60);
const floor = k.h - (piles[col]||0) - it.size*0.6;
if (it.y >= floor){
it.y = floor; it.landed = true;
piles[col] = (piles[col]||0) + it.size*1.1;
if ((piles[col]||0) > k.h*0.8) piles[col] = 0; // reset if too high
}
}
ctx.fillStyle = it.c;
ctx.font = `700 ${it.size}px system-ui,sans-serif`;
ctx.fillText(it.text, it.x, it.y);
}
// prune old landed
if (items.length > 120){
items = items.filter(i => !i.landed).concat(items.filter(i=>i.landed).slice(-60));
}
});
return { stop(){ stop(); k.destroy(); } };
};
})();

198
skills/assets/banner.svg Normal file
View File

@@ -0,0 +1,198 @@
<svg width="1200" height="400" viewBox="0 0 1200 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;900&amp;family=Noto+Serif+SC:wght@700;900&amp;display=swap');
</style>
<!-- Warm accent gradients for mini mockup highlights -->
<linearGradient id="hdBarGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#D4532B"/>
<stop offset="100%" stop-color="#A83518"/>
</linearGradient>
<linearGradient id="hdBarGradSoft" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#8B5E3C"/>
<stop offset="100%" stop-color="#6E4A2E"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="400" fill="#111111"/>
<!-- Left accent line (Pentagram-style editorial vertical rule) -->
<rect x="60" y="48" width="3" height="304" fill="#D4532B"/>
<!-- Top horizontal rule -->
<rect x="60" y="48" width="760" height="2" fill="#FFFFFF" opacity="0.15"/>
<!-- Bottom horizontal rule -->
<rect x="60" y="350" width="760" height="1" fill="#FFFFFF" opacity="0.15"/>
<!-- Thin divider between text and viz -->
<rect x="860" y="80" width="1" height="240" fill="#FFFFFF" opacity="0.08"/>
<!-- ============================================================ -->
<!-- LEFT: TEXT BLOCK -->
<!-- ============================================================ -->
<!-- CATEGORY LABEL -->
<text
x="80"
y="88"
font-family="'Inter', system-ui, -apple-system, sans-serif"
font-size="11"
font-weight="700"
letter-spacing="3"
fill="#D4532B"
>CLAUDE CODE SKILL · DESIGN</text>
<!-- MAIN TITLE -->
<text
x="80"
y="178"
font-family="'Inter', system-ui, -apple-system, sans-serif"
font-size="88"
font-weight="900"
fill="#FFFFFF"
letter-spacing="-3"
>Huashu Design</text>
<!-- Chinese subtitle -->
<text
x="80"
y="222"
font-family="'Noto Serif SC', 'Source Han Serif', 'Inter', serif"
font-size="22"
font-weight="700"
fill="#EEEEEE"
letter-spacing="1"
>用 HTML 做设计的 skill</text>
<!-- Tagline -->
<text
x="80"
y="284"
font-family="'Inter', system-ui, -apple-system, sans-serif"
font-size="15"
font-weight="500"
fill="#BBBBBB"
letter-spacing="0.5"
>高保真原型</text>
<text x="176" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
<text x="188" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">幻灯片</text>
<text x="260" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
<text x="272" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">动画</text>
<text x="320" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
<text x="332" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">信息图</text>
<text x="404" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="700" fill="#D4532B">·</text>
<text x="416" y="284" font-family="'Inter', sans-serif" font-size="15" font-weight="500" fill="#BBBBBB" letter-spacing="0.5">App 原型</text>
<!-- Second tagline row -->
<text
x="80"
y="312"
font-family="'Inter', system-ui, -apple-system, sans-serif"
font-size="14"
font-weight="400"
fill="#888888"
letter-spacing="0.3"
>20 种设计哲学 · 5 维专家评审 · 发布会级动画导出</text>
<!-- Footer credit -->
<text
x="80"
y="370"
font-family="'Inter', system-ui, -apple-system, sans-serif"
font-size="12"
font-weight="400"
fill="#666666"
letter-spacing="0.3"
>for Claude Code &amp; Agent-agnostic</text>
<!-- ============================================================ -->
<!-- RIGHT: MINI MOCKUP GRID (2×2) -->
<!-- Each mock represents one output form of huashu-design -->
<!-- Viewport right area: x 880-1160, y 90-330 -->
<!-- 2×2 grid, tile ≈ 128×104, gap 16 -->
<!-- ============================================================ -->
<!-- Section label -->
<text x="890" y="108" font-family="'Inter', sans-serif" font-size="10" font-weight="700" letter-spacing="2" fill="#D4532B" opacity="0.9">OUTPUT SURFACES</text>
<!-- Grid coordinates:
Col1 x=890 (width 128) Col2 x=1034 (width 128)
Row1 y=122 (height 100) Row2 y=238 (height 100) -->
<!-- ============ TILE 1 · SLIDES (top-left) ============ -->
<rect x="890" y="122" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
<!-- slide stack visual: 3 stacked rectangles offset to imply deck -->
<rect x="902" y="138" width="88" height="56" fill="#2A2A2A" stroke="#3A3A3A" stroke-width="0.5"/>
<rect x="906" y="142" width="88" height="56" fill="#353535"/>
<rect x="910" y="146" width="88" height="56" fill="#E8E2D4"/>
<!-- slide headline stripes -->
<rect x="916" y="152" width="48" height="3" fill="#111111"/>
<rect x="916" y="160" width="72" height="1.5" fill="#666666"/>
<rect x="916" y="166" width="60" height="1.5" fill="#666666"/>
<rect x="916" y="176" width="32" height="14" fill="#D4532B"/>
<!-- tile label -->
<text x="902" y="216" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">SLIDES</text>
<!-- ============ TILE 2 · PROTOTYPE iPhone (top-right) ============ -->
<rect x="1034" y="122" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
<!-- iPhone outline inside tile -->
<rect x="1080" y="130" width="36" height="76" rx="6" fill="#0A0A0A" stroke="#444444" stroke-width="1"/>
<!-- Dynamic island -->
<rect x="1092" y="134" width="12" height="3" rx="1.5" fill="#000000"/>
<!-- Screen content area -->
<rect x="1083" y="140" width="30" height="58" fill="#EEEAE0"/>
<!-- Tiny app UI elements -->
<rect x="1086" y="144" width="24" height="4" fill="#111111"/>
<rect x="1086" y="152" width="16" height="1.5" fill="#888888"/>
<rect x="1086" y="157" width="20" height="1.5" fill="#888888"/>
<rect x="1086" y="164" width="24" height="12" fill="#D4532B"/>
<rect x="1086" y="180" width="11" height="14" fill="#D1CAB8"/>
<rect x="1099" y="180" width="11" height="14" fill="#D1CAB8"/>
<!-- Home indicator -->
<rect x="1092" y="201" width="12" height="1" rx="0.5" fill="#444444"/>
<!-- tile label -->
<text x="1046" y="216" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">PROTOTYPE</text>
<!-- ============ TILE 3 · ANIMATION storyboard (bottom-left) ============ -->
<rect x="890" y="238" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
<!-- 3 storyboard frames in a row -->
<rect x="898" y="252" width="34" height="44" fill="#252525" stroke="#3A3A3A" stroke-width="0.5"/>
<rect x="939" y="252" width="34" height="44" fill="#2E2E2E" stroke="#3A3A3A" stroke-width="0.5"/>
<rect x="980" y="252" width="34" height="44" fill="#353535" stroke="#3A3A3A" stroke-width="0.5"/>
<!-- motion dots -->
<circle cx="910" cy="274" r="6" fill="#666666"/>
<circle cx="956" cy="274" r="6" fill="#9C6A46"/>
<circle cx="997" cy="274" r="6" fill="#D4532B"/>
<!-- motion arc dashes -->
<path d="M 910 274 Q 933 258 956 274" stroke="#D4532B" stroke-width="0.8" fill="none" stroke-dasharray="2 2" opacity="0.6"/>
<path d="M 956 274 Q 977 258 997 274" stroke="#D4532B" stroke-width="0.8" fill="none" stroke-dasharray="2 2" opacity="0.6"/>
<!-- timeline ruler -->
<rect x="898" y="306" width="116" height="1" fill="#555555"/>
<rect x="898" y="306" width="2" height="4" fill="#D4532B"/>
<rect x="938" y="306" width="2" height="4" fill="#555555"/>
<rect x="978" y="306" width="2" height="4" fill="#555555"/>
<rect x="1012" y="306" width="2" height="4" fill="#555555"/>
<!-- tile label -->
<text x="902" y="332" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">ANIMATION</text>
<!-- ============ TILE 4 · INFOGRAPHIC bars (bottom-right) ============ -->
<rect x="1034" y="238" width="128" height="100" rx="2" fill="#1A1A1A" stroke="#333333" stroke-width="1"/>
<!-- bars chart -->
<rect x="1046" y="290" width="12" height="20" fill="url(#hdBarGradSoft)"/>
<rect x="1062" y="278" width="12" height="32" fill="url(#hdBarGradSoft)"/>
<rect x="1078" y="270" width="12" height="40" fill="url(#hdBarGradSoft)"/>
<rect x="1094" y="262" width="12" height="48" fill="url(#hdBarGrad)"/>
<rect x="1110" y="254" width="12" height="56" fill="url(#hdBarGrad)"/>
<rect x="1126" y="248" width="12" height="62" fill="url(#hdBarGrad)"/>
<!-- baseline -->
<rect x="1044" y="310" width="104" height="1" fill="#555555"/>
<!-- headline at top of tile -->
<rect x="1046" y="252" width="50" height="3" fill="#FFFFFF" opacity="0.85"/>
<rect x="1046" y="260" width="34" height="1.5" fill="#666666"/>
<!-- tile label -->
<text x="1046" y="332" font-family="'Inter', sans-serif" font-size="9" font-weight="500" letter-spacing="2" fill="#777777">INFOGRAPHIC</text>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

150
skills/assets/base.css Normal file
View File

@@ -0,0 +1,150 @@
/* html-ppt :: base.css — reset + shared tokens + layout primitives */
/* Default tokens. Themes in assets/themes/*.css override the :root block. */
:root {
--bg: #ffffff;
--bg-soft: #f7f7f8;
--surface: #ffffff;
--surface-2: #f2f2f4;
--border: rgba(0,0,0,.08);
--border-strong: rgba(0,0,0,.16);
--text-1: #111216;
--text-2: #55596a;
--text-3: #8a8f9e;
--accent: #3b6cff;
--accent-2: #7a5cff;
--accent-3: #ff5c8a;
--good: #1aaf6c;
--warn: #f5a524;
--bad: #e0445a;
--grad: linear-gradient(135deg,#3b6cff,#7a5cff 55%,#ff5c8a);
--grad-soft: linear-gradient(135deg,#eef2ff,#f5ecff 55%,#ffeef5);
--radius: 18px;
--radius-sm: 12px;
--radius-lg: 26px;
--shadow: 0 10px 30px rgba(18,24,40,.08), 0 2px 6px rgba(18,24,40,.04);
--shadow-lg: 0 24px 60px rgba(18,24,40,.14), 0 6px 16px rgba(18,24,40,.06);
--font-sans: 'Inter','Noto Sans SC',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
--font-serif: 'Playfair Display','Noto Serif SC',Georgia,serif;
--font-mono: 'JetBrains Mono','IBM Plex Mono',SFMono-Regular,Menlo,monospace;
--font-display: var(--font-sans);
--letter-tight: -.03em;
--letter-normal: -.01em;
--ease: cubic-bezier(.4,0,.2,1);
}
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text-1);
font-family:var(--font-sans);font-weight:400;line-height:1.6;
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
letter-spacing:var(--letter-normal)}
img,svg,video{max-width:100%;display:block}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
code,kbd,pre,samp{font-family:var(--font-mono)}
/* ================= SLIDE SYSTEM ================= */
.deck{position:relative;width:100vw;height:100vh;overflow:hidden;background:var(--bg)}
.slide{
position:absolute;inset:0;
display:flex;flex-direction:column;justify-content:center;
padding:72px 96px;
box-sizing:border-box;
opacity:0;pointer-events:none;
transition:opacity .5s var(--ease), transform .5s var(--ease);
transform:translateX(30px);
overflow:hidden;
}
.slide.is-active{opacity:1;pointer-events:auto;transform:translateX(0);z-index:2}
.slide.is-prev{transform:translateX(-30px)}
/* single-page standalone (used when a layout file is opened directly) */
body.single .slide{position:relative;width:100vw;height:100vh;opacity:1;transform:none;pointer-events:auto}
/* ================= TYPOGRAPHY ================= */
.eyebrow{font-size:13px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--text-3)}
.kicker{font-size:14px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase}
h1.title,.h1{font-family:var(--font-display);font-size:72px;line-height:1.05;font-weight:800;letter-spacing:var(--letter-tight);margin:0 0 18px;color:var(--text-1)}
h2.title,.h2{font-family:var(--font-display);font-size:54px;line-height:1.1;font-weight:700;letter-spacing:var(--letter-tight);margin:0 0 14px}
h3,.h3{font-size:32px;line-height:1.2;font-weight:600;letter-spacing:var(--letter-normal);margin:0 0 10px}
h4,.h4{font-size:22px;line-height:1.3;font-weight:600;margin:0 0 8px}
.lede{font-size:22px;line-height:1.55;color:var(--text-2);font-weight:300;max-width:62ch}
.dim{color:var(--text-2)}
.dim2{color:var(--text-3)}
.mono{font-family:var(--font-mono)}
.serif{font-family:var(--font-serif)}
.gradient-text{background:var(--grad);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;color:transparent}
/* ================= LAYOUT PRIMITIVES ================= */
.stack>*+*{margin-top:14px}
.row{display:flex;gap:24px;align-items:center}
.row.wrap{flex-wrap:wrap}
.grid{display:grid;gap:24px}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
.g4{grid-template-columns:repeat(4,1fr)}
.center{display:flex;align-items:center;justify-content:center;text-align:center}
.fill{flex:1}
.sp-t{padding-top:24px}.sp-b{padding-bottom:24px}
.mt-s{margin-top:8px}.mt-m{margin-top:18px}.mt-l{margin-top:32px}
.mb-s{margin-bottom:8px}.mb-m{margin-bottom:18px}.mb-l{margin-bottom:32px}
/* ================= CARDS ================= */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
padding:26px 28px;box-shadow:var(--shadow);position:relative;overflow:hidden}
.card-soft{background:var(--surface-2);border:1px solid var(--border)}
.card-outline{background:transparent;border:1.5px solid var(--border-strong);box-shadow:none}
.card-accent{background:var(--surface);border-top:3px solid var(--accent)}
.card-hover{transition:transform .3s var(--ease),box-shadow .3s var(--ease)}
.card-hover:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}
.pill{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:500;
background:var(--surface-2);color:var(--text-2);border:1px solid var(--border)}
.pill-accent{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);border-color:color-mix(in srgb,var(--accent) 28%,transparent)}
/* ================= BARS / DIVIDERS ================= */
.divider{height:1px;background:var(--border);width:100%}
.divider-accent{height:3px;width:72px;background:var(--accent);border-radius:2px}
/* ================= CHROME (header/footer/progress) ================= */
.deck-header{position:absolute;top:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
font-size:12px;color:var(--text-3);letter-spacing:.12em;text-transform:uppercase;z-index:10;pointer-events:none}
.deck-footer{position:absolute;bottom:24px;left:40px;right:40px;display:flex;align-items:center;justify-content:space-between;
font-size:12px;color:var(--text-3);z-index:10;pointer-events:none}
.slide-number::before{content:attr(data-current)}
.slide-number::after{content:" / " attr(data-total)}
.progress-bar{position:fixed;left:0;right:0;bottom:0;height:3px;background:transparent;z-index:20}
.progress-bar > span{display:block;height:100%;width:0;background:var(--accent);transition:width .3s var(--ease)}
/* ================= PRESENTER / OVERVIEW ================= */
.notes{display:none!important}
.notes-overlay{position:fixed;inset:auto 0 0 0;max-height:42vh;background:rgba(20,22,30,.95);color:#e8ebf4;
padding:20px 32px;font-size:16px;line-height:1.6;border-top:1px solid rgba(255,255,255,.1);transform:translateY(100%);
transition:transform .3s var(--ease);z-index:40;overflow:auto;font-family:var(--font-sans)}
.notes-overlay.open{transform:translateY(0)}
.overview{position:fixed;inset:0;background:rgba(10,12,18,.92);backdrop-filter:blur(12px);z-index:50;
display:none;padding:40px;overflow:auto}
.overview.open{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;align-content:start}
.overview .thumb{background:var(--surface);border:1px solid var(--border);border-radius:12px;
aspect-ratio:16/9;overflow:hidden;cursor:pointer;position:relative;color:var(--text-1);padding:16px;
font-size:11px;transition:transform .2s var(--ease)}
.overview .thumb:hover{transform:scale(1.04)}
.overview .thumb .n{position:absolute;top:8px;left:10px;font-weight:700;font-size:14px;color:var(--text-3)}
.overview .thumb .t{position:absolute;bottom:10px;left:14px;right:14px;font-weight:600;color:var(--text-1)}
/* ================= PRESENTER VIEW ================= */
/* Presenter view opens in a separate popup window (S key).
* All presenter styles are self-contained in the popup HTML generated by runtime.js.
* The audience window (this file) is NOT affected — it stays as normal deck view.
* Only the .notes class below is needed to hide speaker notes from audience. */
/* ================= UTILITY ================= */
.hidden{display:none!important}
.nowrap{white-space:nowrap}
.tr{text-align:right}.tc{text-align:center}.tl{text-align:left}
.uppercase{text-transform:uppercase;letter-spacing:.12em}
/* ================= PRINT ================= */
@media print{
.slide{position:relative;opacity:1!important;transform:none!important;page-break-after:always;height:100vh}
.deck-header,.deck-footer,.progress-bar,.notes-overlay,.overview{display:none!important}
}

BIN
skills/assets/bgm-ad.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
skills/assets/bgm-tech.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,166 @@
/**
* BrowserWindow — 浏览器窗口边框Chrome风格
*
* 含traffic lights + tab bar + URL bar
*
* 用法:
* <BrowserWindow url="https://example.com" title="Example">
* <YourWebPage />
* </BrowserWindow>
*/
const browserWindowStyles = {
window: {
display: 'inline-block',
background: '#fff',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
},
chrome: {
background: '#dee1e6',
paddingTop: 10,
paddingLeft: 10,
paddingRight: 10,
userSelect: 'none',
},
tabRow: {
display: 'flex',
alignItems: 'flex-end',
gap: 6,
position: 'relative',
},
trafficLights: {
display: 'flex',
gap: 8,
alignItems: 'center',
paddingBottom: 10,
marginRight: 8,
},
light: {
width: 12,
height: 12,
borderRadius: '50%',
border: '0.5px solid rgba(0,0,0,0.15)',
},
close: { background: '#ff5f57' },
minimize: { background: '#febc2e' },
maximize: { background: '#28c840' },
tab: {
background: '#fff',
padding: '8px 30px 8px 14px',
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
fontSize: 12,
color: '#222',
fontFamily: '-apple-system, sans-serif',
maxWidth: 220,
display: 'flex',
alignItems: 'center',
gap: 8,
position: 'relative',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
favicon: {
width: 14,
height: 14,
borderRadius: 2,
background: '#999',
flexShrink: 0,
},
navBar: {
background: '#fff',
padding: '8px 14px',
display: 'flex',
alignItems: 'center',
gap: 10,
borderBottom: '1px solid #e5e7eb',
},
navButtons: {
display: 'flex',
gap: 4,
color: '#5f6368',
fontSize: 16,
},
navButton: {
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
cursor: 'pointer',
},
urlBar: {
flex: 1,
background: '#f1f3f4',
borderRadius: 999,
padding: '7px 14px',
fontSize: 13,
color: '#333',
display: 'flex',
alignItems: 'center',
gap: 8,
fontFamily: '-apple-system, sans-serif',
},
lockIcon: {
color: '#5f6368',
fontSize: 12,
},
content: {
position: 'relative',
overflow: 'auto',
background: '#fff',
},
};
function BrowserWindow({
title = 'New Tab',
url = 'https://example.com',
width = 1200,
height = 800,
showTrafficLights = true,
children,
}) {
return (
<div style={browserWindowStyles.window}>
<div style={browserWindowStyles.chrome}>
<div style={browserWindowStyles.tabRow}>
{showTrafficLights && (
<div style={browserWindowStyles.trafficLights}>
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.close }} />
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.minimize }} />
<div style={{ ...browserWindowStyles.light, ...browserWindowStyles.maximize }} />
</div>
)}
<div style={browserWindowStyles.tab}>
<div style={browserWindowStyles.favicon} />
<span>{title}</span>
</div>
</div>
</div>
<div style={browserWindowStyles.navBar}>
<div style={browserWindowStyles.navButtons}>
<div style={browserWindowStyles.navButton}></div>
<div style={browserWindowStyles.navButton}></div>
<div style={browserWindowStyles.navButton}></div>
</div>
<div style={browserWindowStyles.urlBar}>
<span style={browserWindowStyles.lockIcon}>🔒</span>
<span>{url}</span>
</div>
</div>
<div style={{ ...browserWindowStyles.content, width, height }}>
{children}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.BrowserWindow = BrowserWindow;
}

View File

@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Deck · Multi-file Slide Index</title>
<!--
deck_index.html — 多文件 slide deck 的拼接器
配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
· 每页独立作用域CSS/JS 都隔离),一页出 bug 不影响其他页
· 单页可直接在浏览器打开验证,不依赖 JS goTo()
· 多 agent 可并行做不同页merge 时零冲突
· 适合 ≥15 页的讲座/课件/长 deck
用法:
1. 把本文件复制到 deck 根目录,重命名 index.html
2. 在同目录建 slides/ 子目录,放每一页独立 HTML
3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
4. 每张 slide HTML 建议尺寸 1920×1080自带背景/字体;不要依赖外层 CSS
共享资源(如果需要):
· shared/tokens.css — 跨页 CSS 变量(色板/字号)
· shared/chrome.html — 页眉页脚可复用片段
· 每页 HTML 自己 <link> 进去即可
键盘:← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印
-->
<!-- ═══════════════════════════════════════════════════════ -->
<!-- EDIT THIS — deck 所有页按顺序列出 -->
<!-- ═══════════════════════════════════════════════════════ -->
<script>
window.DECK_MANIFEST = [
{ file: "slides/01-cover.html", label: "Cover" },
{ file: "slides/02-quote.html", label: "Opening Quote" },
{ file: "slides/03-intro.html", label: "Self-intro" },
// 继续往下加。file 是相对本文件的路径label 用于计数器
];
// 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
window.DECK_WIDTH = 1920;
window.DECK_HEIGHT = 1080;
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: #0a0a0a;
overflow: hidden;
font-family: -apple-system, "PingFang SC", sans-serif;
}
#stage {
position: fixed;
top: 50%; left: 50%;
transform-origin: top left;
will-change: transform;
background: #fff;
box-shadow: 0 10px 60px rgba(0,0,0,0.4);
/* size set by JS from DECK_WIDTH/HEIGHT */
}
iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
background: #fff;
}
.counter {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.65);
color: #fff;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
letter-spacing: 0.05em;
font-variant-numeric: tabular-nums;
z-index: 100;
user-select: none;
opacity: 0.7;
transition: opacity 0.2s;
}
.counter:hover { opacity: 1; }
.counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
.nav-zone {
position: fixed;
top: 0; bottom: 0;
width: 15%;
cursor: pointer;
z-index: 50;
}
.nav-zone.left { left: 0; }
.nav-zone.right { right: 0; }
.nav-hint {
position: absolute;
top: 50%; transform: translateY(-50%);
width: 44px; height: 44px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.6);
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
opacity: 0;
transition: opacity 0.2s;
}
.nav-zone.left .nav-hint { left: 20px; }
.nav-zone.right .nav-hint { right: 20px; }
.nav-zone:hover .nav-hint { opacity: 1; }
/* Print: one slide per page, no navigation UI */
@media print {
@page { size: 1920px 1080px; margin: 0; }
html, body { background: #fff; overflow: visible; height: auto; }
#stage { position: static; transform: none !important; box-shadow: none; }
.counter, .nav-zone { display: none !important; }
/* In print mode we render all slides sequentially — see JS */
.print-stack { display: block; }
.print-stack iframe {
width: 1920px;
height: 1080px;
page-break-after: always;
display: block;
}
}
</style>
</head>
<body>
<div id="stage">
<iframe id="frame" src="about:blank"></iframe>
</div>
<div class="nav-zone left" id="navL"><div class="nav-hint"></div></div>
<div class="nav-zone right" id="navR"><div class="nav-hint"></div></div>
<div class="counter" id="counter">1 / 1</div>
<!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
<div class="print-stack" id="printStack" style="display:none;"></div>
<script>
(function () {
const W = window.DECK_WIDTH || 1920;
const H = window.DECK_HEIGHT || 1080;
const deck = window.DECK_MANIFEST || [];
const stage = document.getElementById('stage');
const frame = document.getElementById('frame');
const counter = document.getElementById('counter');
const printStack = document.getElementById('printStack');
const storageKey = 'deck-index-' + location.pathname;
let current = 0;
stage.style.width = W + 'px';
stage.style.height = H + 'px';
function fit() {
const s = Math.min(window.innerWidth / W, window.innerHeight / H);
const x = (window.innerWidth - W * s) / 2;
const y = (window.innerHeight - H * s) / 2;
stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
stage.style.top = '0';
stage.style.left = '0';
}
function show(idx) {
if (idx < 0 || idx >= deck.length) return;
current = idx;
frame.src = deck[idx].file;
counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
if (location.hash !== '#' + (idx + 1)) {
history.replaceState(null, '', '#' + (idx + 1));
}
}
function next() { show(Math.min(current + 1, deck.length - 1)); }
function prev() { show(Math.max(current - 1, 0)); }
// Keyboard
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch (e.key) {
case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
case 'Home': e.preventDefault(); show(0); break;
case 'End': e.preventDefault(); show(deck.length - 1); break;
case 'p': case 'P': window.print(); break;
default:
if (e.key >= '1' && e.key <= '9') {
const i = parseInt(e.key, 10) - 1;
if (i < deck.length) { e.preventDefault(); show(i); }
}
}
});
document.getElementById('navL').addEventListener('click', prev);
document.getElementById('navR').addEventListener('click', next);
window.addEventListener('resize', fit);
window.addEventListener('hashchange', () => {
const m = location.hash.match(/^#(\d+)$/);
if (m) show(parseInt(m[1], 10) - 1);
});
// Initial: hash > localStorage > 0
const hashMatch = location.hash.match(/^#(\d+)$/);
if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
else try {
const v = parseInt(localStorage.getItem(storageKey), 10);
if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
} catch (_) {}
fit();
show(current);
// Print: build a stack of all iframes so browser prints every slide
window.addEventListener('beforeprint', () => {
printStack.innerHTML = '';
deck.forEach(item => {
const f = document.createElement('iframe');
f.src = item.file;
printStack.appendChild(f);
});
printStack.style.display = 'block';
document.getElementById('stage').style.display = 'none';
});
window.addEventListener('afterprint', () => {
printStack.innerHTML = '';
printStack.style.display = 'none';
document.getElementById('stage').style.display = '';
});
})();
</script>
</body>
</html>

420
skills/assets/deck_stage.js Normal file
View File

@@ -0,0 +1,420 @@
/**
* <deck-stage> — HTML幻灯片外壳web component
*
* 提供功能:
* - 固定尺寸canvas默认1920×1080+ auto-scale + letterbox
* - 键盘导航(←/→/Space/Home/End/Esc
* - 左右点击区域导航
* - slide counter (当前/总数)
* - localStorage持久化当前slide
* - Speaker notes postMessage (支持外层渲染)
* - Hash导航 (#slide-5 跳到第5张)
* - Print-to-PDF支持 (Cmd+P / Ctrl+P 一页一slide)
* - 自动给每个slide添加 data-screen-label
*
* 用法:
* <deck-stage>
* <section>Slide 1</section>
* <section>Slide 2</section>
* </deck-stage>
*
* 自定义尺寸:
* <deck-stage width="1080" height="1920">...</deck-stage>
*
* Speaker notes在<head>加
* <script type="application/json" id="speaker-notes">
* ["slide 1 notes", "slide 2 notes"]
* </script>
*/
(function() {
const STORAGE_KEY_PREFIX = 'deck-stage-slide-';
class DeckStage extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._currentSlide = 0;
this._slides = [];
this._storageKey = STORAGE_KEY_PREFIX + (location.pathname || 'default');
}
connectedCallback() {
this._width = parseInt(this.getAttribute('width')) || 1920;
this._height = parseInt(this.getAttribute('height')) || 1080;
// Shadow DOM 先渲染(独立于子节点,不受 parser 时机影响)
this._render();
// 防御:若 script 放在 <head> 里(而非 </deck-stage> 之后),
// parser 此刻可能还没处理完子 <section>querySelectorAll 会返回空。
// 延迟到下一个事件循环,确保子节点都已 parse 完毕。
const init = () => {
this._collectSlides();
this._setupEventListeners();
this._restoreSlide();
this._updateDisplay();
this._setupPrintStyles();
};
if (this.ownerDocument.readyState === 'loading') {
// 文档还在 parse等 DOMContentLoaded 一次搞定所有 section
this.ownerDocument.addEventListener('DOMContentLoaded', init, { once: true });
} else {
// 文档已 parse 完script 在 body 底部或 defer下一帧收集即可
requestAnimationFrame(init);
}
}
_render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
position: fixed;
inset: 0;
background: #000;
overflow: hidden;
font-family: -apple-system, 'SF Pro Text', 'PingFang SC', sans-serif;
}
:host([noscale]) .stage {
transform: none !important;
top: 0 !important;
left: 0 !important;
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform-origin: top left;
will-change: transform;
background: #fff;
}
.slide-wrapper {
width: 100%;
height: 100%;
position: relative;
}
::slotted(section) {
display: none;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
::slotted(section.active) {
display: block;
}
.counter {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
padding: 6px 14px;
border-radius: 999px;
font-size: 13px;
font-variant-numeric: tabular-nums;
z-index: 100;
user-select: none;
opacity: 0.6;
transition: opacity 0.2s;
}
.counter:hover {
opacity: 1;
}
.nav-zone {
position: fixed;
top: 0;
bottom: 0;
width: 15%;
cursor: pointer;
z-index: 50;
}
.nav-zone.left { left: 0; }
.nav-zone.right { right: 0; }
.nav-hint {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 44px;
height: 44px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
opacity: 0;
transition: opacity 0.2s;
}
.nav-zone.left .nav-hint { left: 20px; }
.nav-zone.right .nav-hint { right: 20px; }
.nav-zone:hover .nav-hint {
opacity: 1;
}
@media print {
:host {
position: static;
background: #fff;
}
.counter, .nav-zone {
display: none !important;
}
.stage {
position: static;
transform: none !important;
page-break-after: always;
}
::slotted(section) {
display: block !important;
position: relative !important;
page-break-after: always;
width: 100%;
height: 100%;
}
}
</style>
<div class="stage" id="stage" style="width: ${this._width}px; height: ${this._height}px;">
<div class="slide-wrapper">
<slot></slot>
</div>
</div>
<div class="nav-zone left" id="navLeft">
<div class="nav-hint"></div>
</div>
<div class="nav-zone right" id="navRight">
<div class="nav-hint"></div>
</div>
<div class="counter" id="counter">1 / 1</div>
`;
}
_collectSlides() {
this._slides = Array.from(this.querySelectorAll(':scope > section'));
this._slides.forEach((slide, idx) => {
if (!slide.hasAttribute('data-screen-label')) {
const num = String(idx + 1).padStart(2, '0');
slide.setAttribute('data-screen-label', num);
}
if (!slide.hasAttribute('data-om-validate')) {
slide.setAttribute('data-om-validate', '');
}
});
}
_setupEventListeners() {
window.addEventListener('resize', () => this._updateScale());
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, [contenteditable]')) return;
switch (e.key) {
case 'ArrowRight':
case ' ':
case 'PageDown':
e.preventDefault();
this.next();
break;
case 'ArrowLeft':
case 'PageUp':
e.preventDefault();
this.prev();
break;
case 'Home':
e.preventDefault();
this.goTo(0);
break;
case 'End':
e.preventDefault();
this.goTo(this._slides.length - 1);
break;
}
});
this.shadowRoot.getElementById('navLeft').addEventListener('click', () => this.prev());
this.shadowRoot.getElementById('navRight').addEventListener('click', () => this.next());
window.addEventListener('hashchange', () => this._handleHash());
if (location.hash) {
setTimeout(() => this._handleHash(), 0);
}
const observer = new MutationObserver(() => {
if (this.hasAttribute('noscale')) {
this._updateScale();
}
});
observer.observe(this, { attributes: true, attributeFilter: ['noscale'] });
}
_handleHash() {
const match = location.hash.match(/^#slide-(\d+)$/);
if (match) {
const idx = parseInt(match[1]) - 1;
if (idx >= 0 && idx < this._slides.length) {
this.goTo(idx);
}
}
}
_restoreSlide() {
try {
const stored = localStorage.getItem(this._storageKey);
if (stored !== null) {
const idx = parseInt(stored);
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
}
}
} catch (e) {}
}
_saveSlide() {
try {
localStorage.setItem(this._storageKey, String(this._currentSlide));
} catch (e) {}
}
_updateScale() {
if (this.hasAttribute('noscale')) {
const stage = this.shadowRoot.getElementById('stage');
stage.style.transform = 'none';
stage.style.top = '0';
stage.style.left = '0';
return;
}
const stage = this.shadowRoot.getElementById('stage');
if (!stage) return;
const viewportW = window.innerWidth;
const viewportH = window.innerHeight;
const scale = Math.min(viewportW / this._width, viewportH / this._height);
const scaledW = this._width * scale;
const scaledH = this._height * scale;
const offsetX = (viewportW - scaledW) / 2;
const offsetY = (viewportH - scaledH) / 2;
stage.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
stage.style.top = '0';
stage.style.left = '0';
}
_updateDisplay() {
this._slides.forEach((slide, idx) => {
slide.classList.toggle('active', idx === this._currentSlide);
});
const counter = this.shadowRoot.getElementById('counter');
if (counter) {
counter.textContent = `${this._currentSlide + 1} / ${this._slides.length}`;
}
this._updateScale();
try {
window.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
} catch (e) {}
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
slideIndexChanged: this._currentSlide,
totalSlides: this._slides.length
}, '*');
}
} catch (e) {}
}
_setupPrintStyles() {
const printStyle = document.createElement('style');
printStyle.textContent = `
@media print {
@page {
size: ${this._width}px ${this._height}px;
margin: 0;
}
body {
margin: 0;
padding: 0;
}
deck-stage {
position: static !important;
}
deck-stage > section {
display: block !important;
position: relative !important;
width: ${this._width}px !important;
height: ${this._height}px !important;
page-break-after: always;
overflow: hidden;
}
deck-stage > section:last-child {
page-break-after: auto;
}
}
`;
document.head.appendChild(printStyle);
}
next() {
if (this._currentSlide < this._slides.length - 1) {
this._currentSlide++;
this._saveSlide();
this._updateDisplay();
}
}
prev() {
if (this._currentSlide > 0) {
this._currentSlide--;
this._saveSlide();
this._updateDisplay();
}
}
goTo(idx) {
if (idx >= 0 && idx < this._slides.length) {
this._currentSlide = idx;
this._saveSlide();
this._updateDisplay();
}
}
get currentSlide() {
return this._currentSlide;
}
get totalSlides() {
return this._slides.length;
}
}
customElements.define('deck-stage', DeckStage);
window.DeckStage = DeckStage;
})();

View File

@@ -0,0 +1,205 @@
/**
* DesignCanvas — 变体并排网格布局
*
* 用于展示2+个静态设计variations让用户对比选择。
* 每个variation有label可hover放大。
*
* 用法:
* <DesignCanvas
* title="Hero区设计探索"
* subtitle="3个方向对比"
* columns={3}
* >
* <Variation label="Minimal" description="极简克制版">
* <div>...你的设计1...</div>
* </Variation>
* <Variation label="Editorial" description="杂志编辑风">
* <div>...你的设计2...</div>
* </Variation>
* <Variation label="Brutalist" description="粗粝原始">
* <div>...你的设计3...</div>
* </Variation>
* </DesignCanvas>
*
* 配合React+Babel使用。放在合适的script里然后window.DesignCanvas/window.Variation可用。
*/
const canvasStyles = {
container: {
minHeight: '100vh',
background: '#F5F5F0',
padding: '40px 60px',
fontFamily: '-apple-system, "SF Pro Text", "PingFang SC", sans-serif',
},
header: {
marginBottom: 48,
maxWidth: 900,
},
title: {
fontSize: 36,
fontWeight: 600,
marginBottom: 12,
color: '#1A1A1A',
letterSpacing: '-0.02em',
},
subtitle: {
fontSize: 16,
color: '#666',
lineHeight: 1.5,
},
grid: {
display: 'grid',
gap: 32,
},
cell: {
display: 'flex',
flexDirection: 'column',
gap: 12,
},
cellHeader: {
display: 'flex',
alignItems: 'baseline',
gap: 12,
paddingBottom: 8,
borderBottom: '1px solid #E0E0DA',
},
label: {
fontSize: 14,
fontWeight: 600,
color: '#1A1A1A',
letterSpacing: '-0.01em',
},
description: {
fontSize: 13,
color: '#888',
},
frame: {
background: '#fff',
borderRadius: 4,
border: '1px solid #E0E0DA',
overflow: 'hidden',
position: 'relative',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
cursor: 'pointer',
},
frameInner: {
position: 'relative',
width: '100%',
},
badge: {
position: 'absolute',
top: 12,
left: 12,
background: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
padding: '3px 8px',
borderRadius: 4,
fontSize: 11,
fontWeight: 500,
letterSpacing: '0.5px',
textTransform: 'uppercase',
zIndex: 10,
pointerEvents: 'none',
},
};
function DesignCanvas({ title, subtitle, columns = 3, children }) {
const [expanded, setExpanded] = React.useState(null);
const gridStyle = {
...canvasStyles.grid,
gridTemplateColumns: `repeat(${columns}, 1fr)`,
};
return (
<div style={canvasStyles.container}>
{(title || subtitle) && (
<div style={canvasStyles.header}>
{title && <h1 style={canvasStyles.title}>{title}</h1>}
{subtitle && <p style={canvasStyles.subtitle}>{subtitle}</p>}
</div>
)}
<div style={gridStyle}>
{React.Children.map(children, (child, idx) =>
React.isValidElement(child)
? React.cloneElement(child, {
_index: idx,
_expanded: expanded === idx,
_onToggle: () => setExpanded(expanded === idx ? null : idx),
})
: child
)}
</div>
{expanded !== null && (
<div
onClick={() => setExpanded(null)}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.75)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 40,
cursor: 'zoom-out',
}}
>
<div
onClick={e => e.stopPropagation()}
style={{
background: '#fff',
borderRadius: 8,
overflow: 'hidden',
maxWidth: '90vw',
maxHeight: '90vh',
position: 'relative',
}}
>
{React.Children.toArray(children)[expanded]}
</div>
</div>
)}
</div>
);
}
function Variation({ label, description, number, children, _index, _expanded, _onToggle, aspectRatio = '4 / 3' }) {
const displayNumber = number || String(_index + 1).padStart(2, '0');
return (
<div style={canvasStyles.cell}>
<div style={canvasStyles.cellHeader}>
<span style={{ ...canvasStyles.label, color: '#999', fontFamily: 'ui-monospace, monospace', fontSize: 12 }}>
{displayNumber}
</span>
<span style={canvasStyles.label}>{label}</span>
{description && <span style={canvasStyles.description}> {description}</span>}
</div>
<div
onClick={_onToggle}
style={{
...canvasStyles.frame,
aspectRatio,
}}
onMouseEnter={e => {
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)';
}}
onMouseLeave={e => {
e.currentTarget.style.boxShadow = 'none';
}}
>
<div style={canvasStyles.frameInner}>
{children}
</div>
</div>
</div>
);
}
if (typeof window !== 'undefined') {
Object.assign(window, { DesignCanvas, Variation });
}

167
skills/assets/ecommerce.md Normal file
View File

@@ -0,0 +1,167 @@
<!-- Updated: 2026-02-07 -->
# E-commerce SEO Strategy Template
## Industry Characteristics
- High transaction intent
- Product comparison behavior
- Price sensitivity
- Visual-first decision making
- Seasonal demand patterns
- Competitive marketplace listings
## Recommended Site Architecture
```
/
├── Home
├── /collections (or /categories)
│ ├── /category-1
│ │ ├── /subcategory-1
│ │ └── ...
│ ├── /category-2
│ └── ...
├── /products
│ ├── /product-1
│ ├── /product-2
│ └── ...
├── /brands
│ ├── /brand-1
│ └── ...
├── /sale (or /deals)
├── /new-arrivals
├── /best-sellers
├── /gift-guide
├── /blog
│ ├── /buying-guides
│ ├── /how-to
│ └── /trends
├── /about
├── /contact
├── /shipping
├── /returns
└── /faq
```
## Schema Recommendations
| Page Type | Schema Types |
|-----------|-------------|
| Product Page | Product, Offer, AggregateRating, Review, BreadcrumbList |
| Category Page | CollectionPage, ItemList, BreadcrumbList |
| Brand Page | Brand, Organization |
| Blog | Article, BlogPosting |
### Additional E-commerce Schema (2025)
- **ProductGroup**: Use for products with variants (size, color). Wraps individual Product entries with `variesBy` and `hasVariant` properties. See `schema/templates.json`.
- **Certification**: For product certifications (Energy Star, safety, organic). Replaced EnergyConsumptionDetails (April 2025). Use `hasCertification` on Product.
- **OfferShippingDetails**: Include shipping rate, handling time, and transit time. Critical for Merchant Center eligibility.
> **Google Merchant Center Free Listings:** Products can appear in Google Shopping for free. Ensure Product structured data is in the initial server-rendered HTML (not JavaScript-injected) with required properties: `name`, `image`, `price`, `priceCurrency`, `availability`.
> **JS Rendering Note:** Product structured data should be in initial server-rendered HTML: not dynamically injected via JavaScript (per December 2025 Google JS SEO guidance).
### Product Schema Example
```json
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Product Name",
"image": ["https://example.com/product.jpg"],
"description": "Product description",
"sku": "SKU123",
"brand": {
"@type": "Brand",
"name": "Brand Name"
},
"offers": {
"@type": "Offer",
"price": "99.99",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": "https://example.com/product"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "42"
}
}
```
## Content Requirements
### Product Pages (min 400 words)
- Unique product descriptions (not manufacturer copy)
- Feature highlights
- Use cases / who it's for
- Specifications table
- Size/fit guide (for apparel)
- Care instructions
- Customer reviews
### Category Pages (min 400 words)
- Category introduction
- Buying guide excerpt
- Featured products
- Subcategory links
- Filter/sort options
## Technical Considerations
### Pagination
- Use rel="next"/rel="prev" or load-more
- Ensure all products are crawlable
- Canonical to main category page
### Faceted Navigation
- Noindex filter combinations that create duplicate content
- Use canonical tags appropriately
- Ensure popular filters are indexable
### Product Variations
- Single URL for parent product with variants
- Or separate URLs with canonical to parent
- Structured data for all variants
## Content Priorities
### High Priority
1. Category pages (top level)
2. Best-selling product pages
3. Homepage
4. Buying guides for main categories
### Medium Priority
1. Subcategory pages
2. Brand pages
3. Comparison content
4. Seasonal landing pages
### Blog Topics
- Buying guides ("How to Choose...")
- Product comparisons
- Trend reports
- Use cases and inspiration
- Care and maintenance guides
## Key Metrics to Track
- Revenue from organic search
- Product page rankings
- Category page rankings
- Click-through rate (rich results)
- Average order value from organic
## Generative Engine Optimization (GEO) for E-commerce
AI search platforms increasingly answer product queries directly. Optimize for AI citation:
- [ ] Include clear product specifications, dimensions, materials in structured format
- [ ] Use ProductGroup schema for variant products
- [ ] Provide original product photography with descriptive alt text
- [ ] Include genuine customer review content (AggregateRating schema)
- [ ] Maintain consistent product entity data across all platforms (site, Amazon, Merchant Center)
- [ ] Structure comparison content with clear feature tables AI can parse
- [ ] Add detailed FAQ content for common product questions

View File

@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight.com · Architecture</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Architecture · Diagram Design</p>
<h1>littlemight.com in production</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#5ba8eb"/></marker>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows first (behind boxes) -->
<line x1="168" y1="272" x2="220" y2="272" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<line x1="364" y1="272" x2="416" y2="272" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<line x1="576" y1="256" x2="628" y2="212" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<line x1="576" y1="288" x2="628" y2="332" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="172" y="252" width="48" height="12" rx="2" fill="#1c1a17"/>
<text x="196" y="262" fill="#5ba8eb" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
<rect x="368" y="252" width="48" height="12" rx="2" fill="#1c1a17"/>
<text x="392" y="262" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
<rect x="560" y="212" width="56" height="12" rx="2" fill="#1c1a17"/>
<text x="588" y="222" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
<rect x="560" y="320" width="52" height="12" rx="2" fill="#1c1a17"/>
<text x="586" y="330" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
<!-- Node: Reader -->
<rect x="40" y="240" width="128" height="64" rx="6" fill="#1c1a17"/>
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(142,140,131,0.40)" stroke-width="0.8"/>
<text x="62" y="257" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="104" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="104" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Node: Cloudflare -->
<rect x="220" y="240" width="144" height="64" rx="6" fill="#1c1a17"/>
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
<text x="244" y="257" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="356" y="300" fill="rgba(241,239,231,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
<text x="292" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="292" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Node: Astro (focal coral) -->
<rect x="416" y="240" width="160" height="64" rx="6" fill="#1c1a17"/>
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
<text x="440" y="257" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="568" y="300" fill="rgba(255,106,48,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
<text x="496" y="276" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="496" y="292" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Node: MDX Bundle -->
<rect x="628" y="160" width="144" height="64" rx="6" fill="#1c1a17"/>
<rect x="628" y="160" width="144" height="64" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="652" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
<text x="700" y="196" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
<text x="700" y="212" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
<!-- Node: Content CMS -->
<rect x="628" y="320" width="144" height="64" rx="6" fill="#1c1a17"/>
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(168,166,157,0.50)" stroke-width="0.8"/>
<text x="650" y="337" fill="#a8a69d" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
<text x="700" y="356" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
<text x="700" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
<!-- Legend strip -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<text x="60" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="200" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
<text x="360" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<text x="456" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
<text x="548" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">External</text>
<line x1="636" y1="442" x2="664" y2="442" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="672" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<line x1="784" y1="442" x2="812" y2="442" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="820" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight.com · Architecture</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed; --color-paper-2: #efeee5;
--color-ink: #0b0d0b; --color-muted: #52534e; --color-soft: #65655c;
--color-rule: rgba(11,13,11,0.12);
--color-accent: #f7591f; --color-accent-tint: rgba(247,89,31,0.08);
--color-link: #1a70c7;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; letter-spacing: -0.005em; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Architecture · Diagram Design</p>
<h1>littlemight.com in production</h1>
<p class="subtitle">The static-first stack: Cloudflare's edge absorbs most reads, Astro renders MDX on misses, content is checked into the repo.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows first (behind boxes) -->
<line x1="168" y1="272" x2="220" y2="272" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<line x1="364" y1="272" x2="416" y2="272" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<line x1="576" y1="256" x2="628" y2="212" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<line x1="576" y1="288" x2="628" y2="332" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="172" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="196" y="262" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
<rect x="368" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="392" y="262" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
<rect x="560" y="212" width="56" height="12" rx="2" fill="#f5f4ed"/>
<text x="588" y="222" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
<rect x="560" y="320" width="52" height="12" rx="2" fill="#f5f4ed"/>
<text x="586" y="330" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
<!-- Node: Reader -->
<rect x="40" y="240" width="128" height="64" rx="6" fill="#f5f4ed"/>
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
<text x="62" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="104" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="104" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Node: Cloudflare -->
<rect x="220" y="240" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="244" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="356" y="300" fill="rgba(11,13,11,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
<text x="292" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="292" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Node: Astro (focal coral) -->
<rect x="416" y="240" width="160" height="64" rx="6" fill="#f5f4ed"/>
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="440" y="257" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="568" y="300" fill="rgba(247,89,31,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
<text x="496" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="496" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Node: MDX Bundle -->
<rect x="628" y="160" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="628" y="160" width="144" height="64" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="652" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
<text x="700" y="196" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
<text x="700" y="212" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
<!-- Node: Content CMS -->
<rect x="628" y="320" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
<text x="650" y="337" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
<text x="700" y="356" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
<text x="700" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
<!-- Legend strip -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<text x="360" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="456" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<text x="548" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">External</text>
<line x1="636" y1="442" x2="664" y2="442" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="672" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<line x1="784" y1="442" x2="812" y2="442" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="820" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Edge absorbs the reads</h3></div>
<p>Nearly every reader is served by Cloudflare's edge cache. The Astro origin only wakes on a cold slug or a revalidation.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Content lives in the repo</h3></div>
<ul><li>Posts are MDX files</li><li>Checked in, reviewed in PRs</li><li>No runtime database</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>CMS holds the big stuff</h3></div>
<p>Images, OG art, and downloadable assets live in a separate bucket keyed by slug. Astro links them at render time.</p>
</div>
</div>
<div class="footer">
<span>littlemight.com · architecture</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight.com · Architecture</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Architecture · Diagram Design</p>
<h1>littlemight.com in production</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows first (behind boxes) -->
<line x1="168" y1="272" x2="220" y2="272" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<line x1="364" y1="272" x2="416" y2="272" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<line x1="576" y1="256" x2="628" y2="212" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<line x1="576" y1="288" x2="628" y2="332" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="172" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="196" y="262" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS</text>
<rect x="368" y="252" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="392" y="262" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SSR</text>
<rect x="560" y="212" width="56" height="12" rx="2" fill="#f5f4ed"/>
<text x="588" y="222" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">READ MDX</text>
<rect x="560" y="320" width="52" height="12" rx="2" fill="#f5f4ed"/>
<text x="586" y="330" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">QUERY</text>
<!-- Node: Reader -->
<rect x="40" y="240" width="128" height="64" rx="6" fill="#f5f4ed"/>
<rect x="40" y="240" width="128" height="64" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<rect x="48" y="248" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
<text x="62" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="104" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="104" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Node: Cloudflare -->
<rect x="220" y="240" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="220" y="240" width="144" height="64" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<rect x="228" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="244" y="257" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="356" y="300" fill="rgba(11,13,11,0.06)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">01</text>
<text x="292" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="292" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Node: Astro (focal coral) -->
<rect x="416" y="240" width="160" height="64" rx="6" fill="#f5f4ed"/>
<rect x="416" y="240" width="160" height="64" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="424" y="248" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="440" y="257" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="568" y="300" fill="rgba(247,89,31,0.10)" font-size="32" font-weight="600" font-family="'Geist Mono', monospace" text-anchor="end">02</text>
<text x="496" y="276" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="496" y="292" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Node: MDX Bundle -->
<rect x="628" y="160" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="628" y="160" width="144" height="64" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="636" y="168" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="652" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">BUN</text>
<text x="700" y="196" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">MDX Bundle</text>
<text x="700" y="212" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/*.mdx</text>
<!-- Node: Content CMS -->
<rect x="628" y="320" width="144" height="64" rx="6" fill="#f5f4ed"/>
<rect x="628" y="320" width="144" height="64" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<rect x="636" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
<text x="650" y="337" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CMS</text>
<text x="700" y="356" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Content CMS</text>
<text x="700" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">assets · og images</text>
<!-- Legend strip -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal / origin</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Backend / bundle</text>
<rect x="340" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<text x="360" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Store</text>
<rect x="436" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="456" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cloud</text>
<rect x="528" y="436" width="14" height="10" rx="2" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<text x="548" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">External</text>
<line x1="636" y1="442" x2="664" y2="442" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="672" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<line x1="784" y1="442" x2="812" y2="442" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="820" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary flow</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight content model · ER</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">ER · Diagram Design</p>
<h1>littlemight content model</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Relationship lines (drawn first) -->
<!-- 1. Author — Article (1:N "writes") -->
<line x1="260" y1="240" x2="400" y2="240" stroke="#a8a69d" stroke-width="1" />
<!-- 2. Article — ArticleTag (1:N) -->
<line x1="640" y1="320" x2="780" y2="328" stroke="#a8a69d" stroke-width="1" />
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
<line x1="880" y1="248" x2="880" y2="280" stroke="#a8a69d" stroke-width="1" />
<!-- Cardinality labels -->
<rect x="266" y="232" width="12" height="12" rx="2" fill="#1c1a17"/>
<text x="272" y="242" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="378" y="232" width="16" height="12" rx="2" fill="#1c1a17"/>
<text x="386" y="242" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="646" y="316" width="12" height="12" rx="2" fill="#1c1a17"/>
<text x="652" y="326" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="760" y="324" width="16" height="12" rx="2" fill="#1c1a17"/>
<text x="768" y="334" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="872" y="252" width="16" height="12" rx="2" fill="#1c1a17"/>
<text x="880" y="262" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="872" y="268" width="16" height="12" rx="2" fill="#1c1a17"/>
<text x="880" y="278" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<!-- Relationship labels -->
<rect x="304" y="220" width="56" height="14" rx="2" fill="#1c1a17"/>
<text x="332" y="230" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
<rect x="688" y="300" width="56" height="14" rx="2" fill="#1c1a17"/>
<text x="716" y="310" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
<!-- Entity: Author -->
<rect x="60" y="160" width="200" height="160" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(241,239,231,0.04)" stroke="none"/>
<rect x="60" y="192" width="200" height="8" fill="rgba(241,239,231,0.04)"/>
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<text x="76" y="176" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="76" y="192" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
<text x="76" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="220" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="76" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">handle</text>
<text x="220" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="260" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="220" y="260" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="280" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">bio</text>
<text x="220" y="280" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="300" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
<text x="220" y="300" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: Article (focal coral) -->
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(255,106,48,0.04)" stroke="#ff6a30" stroke-width="1"/>
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(255,106,48,0.10)" stroke="none"/>
<rect x="400" y="152" width="240" height="8" fill="rgba(255,106,48,0.10)"/>
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(255,106,48,0.40)" stroke-width="1"/>
<text x="416" y="136" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
<text x="416" y="152" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
<text x="416" y="180" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="600" y="180" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="200" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">title</text>
<text x="600" y="200" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="600" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="416" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
<text x="600" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="260" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
<text x="600" y="260" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
<text x="416" y="280" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
<text x="600" y="280" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="300" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">status</text>
<text x="600" y="300" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
<text x="416" y="320" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
<text x="600" y="320" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
<!-- Entity: Tag -->
<rect x="780" y="120" width="200" height="128" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(241,239,231,0.04)" stroke="none"/>
<rect x="780" y="152" width="200" height="8" fill="rgba(241,239,231,0.04)"/>
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<text x="796" y="136" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="796" y="152" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
<text x="796" y="180" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="940" y="180" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="200" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="940" y="200" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="796" y="220" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="940" y="220" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="796" y="240" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">description</text>
<text x="940" y="240" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: ArticleTag (join) -->
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(241,239,231,0.04)" stroke="#a8a69d" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(241,239,231,0.06)" stroke="none"/>
<rect x="780" y="312" width="200" height="8" fill="rgba(241,239,231,0.06)"/>
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<text x="796" y="296" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
<text x="796" y="312" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
<text x="796" y="340" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
<text x="940" y="340" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="360" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
<text x="940" y="360" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<!-- Legend -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(255,106,48,0.04)" stroke="#ff6a30" stroke-width="1"/>
<text x="60" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="200" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(241,239,231,0.04)" stroke="#a8a69d" stroke-width="1" stroke-dasharray="3,2"/>
<text x="288" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
<text x="372" y="444" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
<text x="388" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
<text x="476" y="444" fill="#f1efe7" font-size="10" font-family="'Geist Mono', monospace" font-weight="600"></text>
<text x="492" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
<text x="584" y="444" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
<text x="616" y="444" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight content model · ER</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">ER · Diagram Design</p>
<h1>littlemight content model</h1>
<p class="subtitle">The four entities behind the site. Article is the aggregate root — everything else exists to describe or classify it. `#` marks primary keys, `→` marks foreign keys.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Relationship lines (drawn first) -->
<!-- 1. Author — Article (1:N "writes") -->
<line x1="260" y1="240" x2="400" y2="240" stroke="#52534e" stroke-width="1" />
<!-- 2. Article — ArticleTag (1:N) -->
<line x1="640" y1="320" x2="780" y2="328" stroke="#52534e" stroke-width="1" />
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
<line x1="880" y1="248" x2="880" y2="280" stroke="#52534e" stroke-width="1" />
<!-- Cardinality labels -->
<rect x="266" y="232" width="12" height="12" rx="2" fill="#f5f4ed"/>
<text x="272" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="378" y="232" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="386" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="646" y="316" width="12" height="12" rx="2" fill="#f5f4ed"/>
<text x="652" y="326" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="760" y="324" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="768" y="334" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="872" y="252" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="880" y="262" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="872" y="268" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="880" y="278" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<!-- Relationship labels -->
<rect x="304" y="220" width="56" height="14" rx="2" fill="#f5f4ed"/>
<text x="332" y="230" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
<rect x="688" y="300" width="56" height="14" rx="2" fill="#f5f4ed"/>
<text x="716" y="310" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
<!-- Entity: Author -->
<rect x="60" y="160" width="200" height="160" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
<rect x="60" y="192" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="76" y="176" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="76" y="192" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
<text x="76" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="220" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="76" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">handle</text>
<text x="220" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="220" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">bio</text>
<text x="220" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
<text x="220" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: Article (focal coral) -->
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(247,89,31,0.10)" stroke="none"/>
<rect x="400" y="152" width="240" height="8" fill="rgba(247,89,31,0.10)"/>
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(247,89,31,0.40)" stroke-width="1"/>
<text x="416" y="136" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
<text x="416" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
<text x="416" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="600" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">title</text>
<text x="600" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="600" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="416" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
<text x="600" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
<text x="600" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
<text x="416" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
<text x="600" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">status</text>
<text x="600" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
<text x="416" y="320" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
<text x="600" y="320" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
<!-- Entity: Tag -->
<rect x="780" y="120" width="200" height="128" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
<rect x="780" y="152" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="796" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="796" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
<text x="796" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="940" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="940" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="796" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="940" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="796" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">description</text>
<text x="940" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: ArticleTag (join) -->
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(11,13,11,0.06)" stroke="none"/>
<rect x="780" y="312" width="200" height="8" fill="rgba(11,13,11,0.06)"/>
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="796" y="296" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
<text x="796" y="312" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
<text x="796" y="340" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
<text x="940" y="340" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="360" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
<text x="940" y="360" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<!-- Legend -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="3,2"/>
<text x="288" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
<text x="372" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
<text x="388" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
<text x="476" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600"></text>
<text x="492" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
<text x="584" y="444" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
<text x="616" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Article is the root</h3></div>
<p>Author, Tag, and the join table only exist to describe Article. If you're thinking about a feature, start here and trace outward.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Many-to-many via a join</h3></div>
<ul><li>Tags aren't embedded on Article</li><li>ArticleTag is a pure join — no metadata</li><li>Dashed border signals it's not a primary entity</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Cardinality over arrows</h3></div>
<p>Plain lines with 1/N at the ends read cleaner than crow's feet at this size. Every relationship carries both numbers.</p>
</div>
</div>
<div class="footer">
<span>littlemight content model · ER</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight content model · ER</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">ER · Diagram Design</p>
<h1>littlemight content model</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Relationship lines (drawn first) -->
<!-- 1. Author — Article (1:N "writes") -->
<line x1="260" y1="240" x2="400" y2="240" stroke="#52534e" stroke-width="1" />
<!-- 2. Article — ArticleTag (1:N) -->
<line x1="640" y1="320" x2="780" y2="328" stroke="#52534e" stroke-width="1" />
<!-- 3. Tag — ArticleTag (1:N, vertical) -->
<line x1="880" y1="248" x2="880" y2="280" stroke="#52534e" stroke-width="1" />
<!-- Cardinality labels -->
<rect x="266" y="232" width="12" height="12" rx="2" fill="#f5f4ed"/>
<text x="272" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="378" y="232" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="386" y="242" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="646" y="316" width="12" height="12" rx="2" fill="#f5f4ed"/>
<text x="652" y="326" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="760" y="324" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="768" y="334" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<rect x="872" y="252" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="880" y="262" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
<rect x="872" y="268" width="16" height="12" rx="2" fill="#f5f4ed"/>
<text x="880" y="278" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">N</text>
<!-- Relationship labels -->
<rect x="304" y="220" width="56" height="14" rx="2" fill="#f5f4ed"/>
<text x="332" y="230" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">WRITES</text>
<rect x="688" y="300" width="56" height="14" rx="2" fill="#f5f4ed"/>
<text x="716" y="310" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">TAGGED</text>
<!-- Entity: Author -->
<rect x="60" y="160" width="200" height="160" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="60" y="160" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
<rect x="60" y="192" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
<line x1="60" y1="200" x2="260" y2="200" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="76" y="176" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="76" y="192" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Author</text>
<text x="76" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="220" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="76" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">handle</text>
<text x="220" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="220" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">bio</text>
<text x="220" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="76" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">site_url</text>
<text x="220" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: Article (focal coral) -->
<rect x="400" y="120" width="240" height="240" rx="6" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
<rect x="400" y="120" width="240" height="40" rx="6" fill="rgba(247,89,31,0.10)" stroke="none"/>
<rect x="400" y="152" width="240" height="8" fill="rgba(247,89,31,0.10)"/>
<line x1="400" y1="160" x2="640" y2="160" stroke="rgba(247,89,31,0.40)" stroke-width="1"/>
<text x="416" y="136" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY · AGGREGATE ROOT</text>
<text x="416" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Article</text>
<text x="416" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="600" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">title</text>
<text x="600" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="600" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="416" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">body_mdx</text>
<text x="600" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="416" y="260" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">published_at</text>
<text x="600" y="260" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">timestamp</text>
<text x="416" y="280" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ author_id</text>
<text x="600" y="280" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="416" y="300" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">status</text>
<text x="600" y="300" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">enum</text>
<text x="416" y="320" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">og_image</text>
<text x="600" y="320" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · url</text>
<!-- Entity: Tag -->
<rect x="780" y="120" width="200" height="128" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="780" y="120" width="200" height="40" rx="6" fill="rgba(11,13,11,0.04)" stroke="none"/>
<rect x="780" y="152" width="200" height="8" fill="rgba(11,13,11,0.04)"/>
<line x1="780" y1="160" x2="980" y2="160" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="796" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">ENTITY</text>
<text x="796" y="152" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">Tag</text>
<text x="796" y="180" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace"># id</text>
<text x="940" y="180" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="200" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">slug</text>
<text x="940" y="200" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text · unique</text>
<text x="796" y="220" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">name</text>
<text x="940" y="220" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<text x="796" y="240" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">description</text>
<text x="940" y="240" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
<!-- Entity: ArticleTag (join) -->
<rect x="780" y="280" width="200" height="96" rx="6" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="780" y="280" width="200" height="40" rx="6" fill="rgba(11,13,11,0.06)" stroke="none"/>
<rect x="780" y="312" width="200" height="8" fill="rgba(11,13,11,0.06)"/>
<line x1="780" y1="320" x2="980" y2="320" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<text x="796" y="296" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">JOIN</text>
<text x="796" y="312" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif">ArticleTag</text>
<text x="796" y="340" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ article_id</text>
<text x="940" y="340" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<text x="796" y="360" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace">→ tag_id</text>
<text x="940" y="360" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
<!-- Legend -->
<line x1="40" y1="404" x2="960" y2="404" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="420" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="436" width="14" height="10" rx="2" fill="rgba(247,89,31,0.04)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Aggregate root</text>
<rect x="180" y="436" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Entity</text>
<rect x="268" y="436" width="14" height="10" rx="2" fill="rgba(11,13,11,0.04)" stroke="#52534e" stroke-width="1" stroke-dasharray="3,2"/>
<text x="288" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Join table</text>
<text x="372" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">#</text>
<text x="388" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary key</text>
<text x="476" y="444" fill="#0b0d0b" font-size="10" font-family="'Geist Mono', monospace" font-weight="600"></text>
<text x="492" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Foreign key</text>
<text x="584" y="444" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" font-weight="600">1 / N</text>
<text x="616" y="444" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Cardinality</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Should you write this as a skill?</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Flowchart · Diagram Design</p>
<h1>Should you write this as a skill?</h1>
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows (drawn first, behind nodes) -->
<!-- Start → Step -->
<line x1="500" y1="88" x2="500" y2="120" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Step → Diamond 1 -->
<line x1="500" y1="168" x2="500" y2="192" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "NO" right -->
<line x1="600" y1="240" x2="720" y2="240" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "YES" down -->
<line x1="500" y1="288" x2="500" y2="328" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "NO" right -->
<line x1="600" y1="376" x2="720" y2="376" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "YES" down (coral — happy path) -->
<line x1="500" y1="424" x2="500" y2="464" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Arrow labels -->
<rect x="644" y="230" width="24" height="12" rx="2" fill="#1c1a17"/>
<text x="656" y="239" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="298" width="32" height="12" rx="2" fill="#1c1a17"/>
<text x="500" y="307" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<rect x="644" y="366" width="24" height="12" rx="2" fill="#1c1a17"/>
<text x="656" y="375" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="434" width="32" height="12" rx="2" fill="#1c1a17"/>
<text x="500" y="443" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<!-- Start oval -->
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<text x="500" y="68" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
<!-- Rectangle: Step -->
<rect x="420" y="120" width="160" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="500" y="148" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
<!-- Diamond 1: Repeated >3 times? -->
<polygon points="500,192 600,240 500,288 400,240" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="500" y="238" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
<text x="500" y="252" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
<!-- End oval: One-off -->
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<text x="800" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
<text x="800" y="254" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
<!-- Diamond 2: Reusable across projects? -->
<polygon points="500,328 600,376 500,424 400,376" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="500" y="374" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
<text x="500" y="388" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
<!-- End oval: CLAUDE.md note -->
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<text x="800" y="376" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
<text x="800" y="390" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
<!-- End oval: Write a skill (coral focal) -->
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<text x="500" y="492" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
<text x="500" y="508" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
<!-- Legend -->
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="556" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<text x="72" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
<rect x="220" y="572" width="24" height="12" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="252" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
<polygon points="412,578 424,572 436,578 424,584" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="448" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
<line x1="604" y1="580" x2="632" y2="580" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="640" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
<line x1="768" y1="580" x2="796" y2="580" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="804" y="582" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Should you write this as a skill?</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Flowchart · Diagram Design</p>
<h1>Should you write this as a skill?</h1>
<p class="subtitle">A three-decision triage for turning a one-off workflow into something reusable. Shape carries type — ovals bracket the flow, rectangles are steps, diamonds are decisions.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows (drawn first, behind nodes) -->
<!-- Start → Step -->
<line x1="500" y1="88" x2="500" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Step → Diamond 1 -->
<line x1="500" y1="168" x2="500" y2="192" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "NO" right -->
<line x1="600" y1="240" x2="720" y2="240" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "YES" down -->
<line x1="500" y1="288" x2="500" y2="328" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "NO" right -->
<line x1="600" y1="376" x2="720" y2="376" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "YES" down (coral — happy path) -->
<line x1="500" y1="424" x2="500" y2="464" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Arrow labels -->
<rect x="644" y="230" width="24" height="12" rx="2" fill="#f5f4ed"/>
<text x="656" y="239" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="298" width="32" height="12" rx="2" fill="#f5f4ed"/>
<text x="500" y="307" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<rect x="644" y="366" width="24" height="12" rx="2" fill="#f5f4ed"/>
<text x="656" y="375" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="434" width="32" height="12" rx="2" fill="#f5f4ed"/>
<text x="500" y="443" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<!-- Start oval -->
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="500" y="68" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
<!-- Rectangle: Step -->
<rect x="420" y="120" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="148" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
<!-- Diamond 1: Repeated >3 times? -->
<polygon points="500,192 600,240 500,288 400,240" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="238" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
<text x="500" y="252" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
<!-- End oval: One-off -->
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="800" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
<text x="800" y="254" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
<!-- Diamond 2: Reusable across projects? -->
<polygon points="500,328 600,376 500,424 400,376" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="374" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
<text x="500" y="388" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
<!-- End oval: CLAUDE.md note -->
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="800" y="376" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
<text x="800" y="390" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
<!-- End oval: Write a skill (coral focal) -->
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="500" y="492" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
<text x="500" y="508" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
<!-- Legend -->
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="556" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="72" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
<rect x="220" y="572" width="24" height="12" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="252" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
<polygon points="412,578 424,572 436,578 424,584" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="448" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
<line x1="604" y1="580" x2="632" y2="580" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="640" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
<line x1="768" y1="580" x2="796" y2="580" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="804" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">WHY THIS FLOWCHART</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Most things aren't skills</h3></div>
<p>The happy path is narrow on purpose. Only workflows that clear all three gates earn the overhead of a reusable skill — everything else is better as a project note.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Shape, not color</h3></div>
<ul><li>Oval bookends the flow</li><li>Rectangle is a step</li><li>Diamond is a decision</li><li>Color reserved for the happy path</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Every branch gets a label</h3></div>
<p>Unlabeled branches turn a flowchart into a maze. Yes/No is fine; conditions in mono when the logic is richer.</p>
</div>
</div>
<div class="footer">
<span>flowchart · should I write this as a skill?</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Should you write this as a skill?</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Flowchart · Diagram Design</p>
<h1>Should you write this as a skill?</h1>
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Arrows (drawn first, behind nodes) -->
<!-- Start → Step -->
<line x1="500" y1="88" x2="500" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Step → Diamond 1 -->
<line x1="500" y1="168" x2="500" y2="192" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "NO" right -->
<line x1="600" y1="240" x2="720" y2="240" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 1 "YES" down -->
<line x1="500" y1="288" x2="500" y2="328" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "NO" right -->
<line x1="600" y1="376" x2="720" y2="376" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Diamond 2 "YES" down (coral — happy path) -->
<line x1="500" y1="424" x2="500" y2="464" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Arrow labels -->
<rect x="644" y="230" width="24" height="12" rx="2" fill="#f5f4ed"/>
<text x="656" y="239" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="298" width="32" height="12" rx="2" fill="#f5f4ed"/>
<text x="500" y="307" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<rect x="644" y="366" width="24" height="12" rx="2" fill="#f5f4ed"/>
<text x="656" y="375" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">NO</text>
<rect x="484" y="434" width="32" height="12" rx="2" fill="#f5f4ed"/>
<text x="500" y="443" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">YES</text>
<!-- Start oval -->
<rect x="420" y="40" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="500" y="68" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">New workflow</text>
<!-- Rectangle: Step -->
<rect x="420" y="120" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="148" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Do it manually once</text>
<!-- Diamond 1: Repeated >3 times? -->
<polygon points="500,192 600,240 500,288 400,240" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="238" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Will you repeat</text>
<text x="500" y="252" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">it >3 times?</text>
<!-- End oval: One-off -->
<rect x="720" y="216" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="800" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">One-off</text>
<text x="800" y="254" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">keep manual</text>
<!-- Diamond 2: Reusable across projects? -->
<polygon points="500,328 600,376 500,424 400,376" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="500" y="374" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reusable across</text>
<text x="500" y="388" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">projects?</text>
<!-- End oval: CLAUDE.md note -->
<rect x="720" y="352" width="160" height="48" rx="24" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="800" y="376" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md note</text>
<text x="800" y="390" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">project-scoped</text>
<!-- End oval: Write a skill (coral focal) -->
<rect x="420" y="464" width="160" height="56" rx="28" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="500" y="492" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Write a skill</text>
<text x="500" y="508" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">reusable + assets</text>
<!-- Legend -->
<line x1="40" y1="540" x2="960" y2="540" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="556" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · SHAPE CARRIES TYPE</text>
<rect x="40" y="572" width="24" height="12" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<text x="72" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start / end (oval)</text>
<rect x="220" y="572" width="24" height="12" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="252" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step (rectangle)</text>
<polygon points="412,578 424,572 436,578 424,584" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="448" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Decision (diamond)</text>
<line x1="604" y1="580" x2="632" y2="580" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="640" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Happy path</text>
<line x1="768" y1="580" x2="796" y2="580" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="804" y="582" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Branch</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI app stack · Layer hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Layer stack · Diagram Design</p>
<h1>AI app stack · Where the work actually happens</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Direction column (left margin) -->
<text x="60" y="68" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<polygon points="76,80 84,80 80,72" fill="#a8a69d"/>
<text x="60" y="416" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
<!-- Stack container hairlines (top + bottom edges) -->
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<!-- L5 — UI (top layer, lifted fill) -->
<rect x="120" y="80" width="840" height="64" fill="#2a2723"/>
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="140" y="116" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
<text x="260" y="118" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
<text x="940" y="118" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
<rect x="120" y="144" width="840" height="64" fill="rgba(255,106,48,0.12)"/>
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#ff6a30" stroke-width="1"/>
<text x="140" y="180" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
<text x="260" y="182" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
<text x="940" y="182" fill="#ff6a30" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
<!-- L3 — Prompt layer -->
<rect x="120" y="208" width="840" height="64" fill="#1c1a17"/>
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="140" y="244" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
<text x="260" y="246" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
<text x="940" y="246" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
<!-- L2 — SDK -->
<rect x="120" y="272" width="840" height="64" fill="#221f1c"/>
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="140" y="308" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
<text x="260" y="310" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
<text x="940" y="310" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
<!-- L1 — Model -->
<rect x="120" y="336" width="840" height="64" fill="#221f1c"/>
<text x="140" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
<text x="260" y="374" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
<text x="940" y="374" fill="#a8a69d" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
<!-- Caption -->
<text x="120" y="456" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
<text x="240" y="456" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI app stack · Layer hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Layer stack · Diagram Design</p>
<h1>AI app stack · Where the work actually happens</h1>
<p class="subtitle">Five layers between silicon and the user. Most teams over-invest in the model and under-invest in the harness — which is the layer that decides whether the app feels magical or flaky.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Direction column (left margin) -->
<text x="60" y="68" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<polygon points="76,80 84,80 80,72" fill="#52534e"/>
<text x="60" y="416" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
<!-- Stack container hairlines (top + bottom edges) -->
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<!-- L5 — UI (top layer, near-white fill) -->
<rect x="120" y="80" width="840" height="64" fill="#ffffff"/>
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="116" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
<text x="260" y="118" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
<text x="940" y="118" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
<rect x="120" y="144" width="840" height="64" fill="rgba(247,89,31,0.08)"/>
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#f7591f" stroke-width="1"/>
<text x="140" y="180" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
<text x="260" y="182" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
<text x="940" y="182" fill="#f7591f" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
<!-- L3 — Prompt layer -->
<rect x="120" y="208" width="840" height="64" fill="#f5f4ed"/>
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="244" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
<text x="260" y="246" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
<text x="940" y="246" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
<!-- L2 — SDK -->
<rect x="120" y="272" width="840" height="64" fill="#efeee5"/>
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="308" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
<text x="260" y="310" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
<text x="940" y="310" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
<!-- L1 — Model -->
<rect x="120" y="336" width="840" height="64" fill="#efeee5"/>
<text x="140" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
<text x="260" y="374" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
<text x="940" y="374" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
<!-- Caption -->
<text x="120" y="456" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
<text x="240" y="456" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Coral marks the layer that pays rent</h3></div>
<p>Swapping models is a knob. Rewriting the harness is a product decision. The coral band says: this is the layer where you out-execute the competition, not the one where you chase leaderboards.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Reading the stack</h3></div>
<ul><li>Abstraction rises up the left column</li><li>L-index on the left, note on the right</li><li>Hairlines between layers, no shadows</li><li>Fill shade shifts from paper-2 to white</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Why only five</h3></div>
<p>Six-plus layers become a legend, not a diagram. Five holds the whole thing on one screen — every band readable without squinting, every note a scan away.</p>
</div>
</div>
<div class="footer">
<span>ai app stack · layer hierarchy</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI app stack · Layer hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Layer stack · Diagram Design</p>
<h1>AI app stack · Where the work actually happens</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Direction column (left margin) -->
<text x="60" y="68" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">ABSTRACTION</text>
<line x1="80" y1="80" x2="80" y2="400" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<polygon points="76,80 84,80 80,72" fill="#52534e"/>
<text x="60" y="416" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">SILICON</text>
<!-- Stack container hairlines (top + bottom edges) -->
<line x1="120" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<line x1="120" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<!-- L5 — UI (top layer, near-white fill) -->
<rect x="120" y="80" width="840" height="64" fill="#ffffff"/>
<line x1="120" y1="144" x2="960" y2="144" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="116" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L5</text>
<text x="260" y="118" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">UI surface</text>
<text x="940" y="118" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">chat, editor, canvas</text>
<!-- L4 — Agent harness (FOCAL, coral tint + coral stroke) -->
<rect x="120" y="144" width="840" height="64" fill="rgba(247,89,31,0.08)"/>
<rect x="120" y="144" width="840" height="64" fill="none" stroke="#f7591f" stroke-width="1"/>
<text x="140" y="180" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">L4</text>
<text x="260" y="182" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Agent harness</text>
<text x="940" y="182" fill="#f7591f" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">tools, memory, loop</text>
<!-- L3 — Prompt layer -->
<rect x="120" y="208" width="840" height="64" fill="#f5f4ed"/>
<line x1="120" y1="272" x2="960" y2="272" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="244" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L3</text>
<text x="260" y="246" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Prompt layer</text>
<text x="940" y="246" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">system, few-shot, caching</text>
<!-- L2 — SDK -->
<rect x="120" y="272" width="840" height="64" fill="#efeee5"/>
<line x1="120" y1="336" x2="960" y2="336" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="140" y="308" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L2</text>
<text x="260" y="310" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">SDK / client</text>
<text x="940" y="310" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">auth, retries, streaming</text>
<!-- L1 — Model -->
<rect x="120" y="336" width="840" height="64" fill="#efeee5"/>
<text x="140" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">L1</text>
<text x="260" y="374" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif">Model weights</text>
<text x="940" y="374" fill="#52534e" font-size="10" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.08em">opus, sonnet, haiku</text>
<!-- Caption -->
<text x="120" y="456" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">FOCAL LAYER</text>
<text x="240" y="456" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">The harness is where most product differentiation actually lives — tools, memory, and the loop that stitches model calls into useful work.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The CLAUDE.md Hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Nested · Diagram Design</p>
<h1>The CLAUDE.md Hierarchy</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Level 1: ~/.claude/ (global) — outermost -->
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(241,239,231,0.015)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<!-- Level 2: ~/vault/ -->
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(241,239,231,0.02)" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
<!-- Level 3: /business -->
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(241,239,231,0.025)" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
<!-- Level 4: /marketing -->
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(241,239,231,0.03)" stroke="#a8a69d" stroke-width="1"/>
<!-- Level 5: /project — innermost, coral focal -->
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<!-- Level labels on paper-colored masks -->
<rect x="56" y="52" width="188" height="16" fill="#1c1a17"/>
<text x="64" y="64" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
<rect x="88" y="88" width="148" height="16" fill="#1c1a17"/>
<text x="96" y="100" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
<rect x="120" y="124" width="96" height="16" fill="#1c1a17"/>
<text x="128" y="136" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
<rect x="152" y="160" width="108" height="16" fill="#1c1a17"/>
<text x="160" y="172" fill="#f1efe7" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
<rect x="184" y="196" width="88" height="16" fill="#1c1a17"/>
<text x="192" y="208" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
<!-- File-icon glyphs (simple rect with folded corner) inside each level -->
<g transform="translate(908, 408)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.35)" stroke-width="1"/>
</g>
<g transform="translate(876, 372)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.40)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.40)" stroke-width="1"/>
</g>
<g transform="translate(844, 336)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="rgba(241,239,231,0.50)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(241,239,231,0.50)" stroke-width="1"/>
</g>
<g transform="translate(812, 300)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#1c1a17" stroke="#a8a69d" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#a8a69d" stroke-width="1"/>
</g>
<!-- Innermost label — human readable -->
<text x="500" y="248" fill="#f1efe7" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
<text x="500" y="272" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
<!-- Annotation top-right: italic callout with curved arrow -->
<text x="904" y="36" fill="#f1efe7" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(241,239,231,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
<circle cx="520" cy="216" r="2" fill="#f1efe7"/>
<!-- Annotation bottom-left: italic callout -->
<text x="40" y="484" fill="#a8a69d" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The CLAUDE.md Hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Nested · Diagram Design</p>
<h1>The CLAUDE.md Hierarchy</h1>
<p class="subtitle">How Claude Code composes context from every folder level above. Each outer ring is a broader scope; the innermost box is where the work happens — inheriting every instruction above it without a single import statement.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Level 1: ~/.claude/ (global) — outermost -->
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(11,13,11,0.015)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<!-- Level 2: ~/vault/ -->
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
<!-- Level 3: /business -->
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(11,13,11,0.025)" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<!-- Level 4: /marketing -->
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(11,13,11,0.03)" stroke="#52534e" stroke-width="1"/>
<!-- Level 5: /project — innermost, coral focal -->
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(247,89,31,0.06)" stroke="#f7591f" stroke-width="1"/>
<!-- Level labels on paper-2-colored masks (match diagram-container bg) -->
<rect x="56" y="52" width="188" height="16" fill="#efeee5"/>
<text x="64" y="64" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
<rect x="88" y="88" width="148" height="16" fill="#efeee5"/>
<text x="96" y="100" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
<rect x="120" y="124" width="96" height="16" fill="#efeee5"/>
<text x="128" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
<rect x="152" y="160" width="108" height="16" fill="#efeee5"/>
<text x="160" y="172" fill="#0b0d0b" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
<rect x="184" y="196" width="88" height="16" fill="#efeee5"/>
<text x="192" y="208" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
<!-- File-icon glyphs inside each level -->
<g transform="translate(908, 408)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
</g>
<g transform="translate(876, 372)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
</g>
<g transform="translate(844, 336)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
</g>
<g transform="translate(812, 300)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#efeee5" stroke="#52534e" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#52534e" stroke-width="1"/>
</g>
<!-- Innermost label — human readable -->
<text x="500" y="248" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
<text x="500" y="272" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
<!-- Annotation top-right: italic callout with curved arrow -->
<text x="904" y="36" fill="#0b0d0b" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
<circle cx="520" cy="216" r="2" fill="#0b0d0b"/>
<!-- Annotation bottom-left: italic callout -->
<text x="40" y="484" fill="#52534e" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Containment is the config</h3></div>
<p>Every CLAUDE.md inside a parent folder implicitly wraps the one below. No manifest, no import graph — the file tree itself declares scope, so renaming a folder is the only migration you'll ever run.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Reads outside-in</h3></div>
<ul><li>Global rules load first</li><li>Each parent narrows context</li><li>Innermost file gets the last word</li><li>Conflicts resolve by specificity</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Five levels is the ceiling</h3></div>
<p>Past five rings the diagram stops teaching — and so does the filesystem. If you need six levels of instruction, you probably need two projects instead.</p>
</div>
</div>
<div class="footer">
<span>the claude.md hierarchy · nested containment</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The CLAUDE.md Hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Nested · Diagram Design</p>
<h1>The CLAUDE.md Hierarchy</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Level 1: ~/.claude/ (global) — outermost -->
<rect x="40" y="60" width="920" height="380" rx="8" fill="rgba(11,13,11,0.015)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<!-- Level 2: ~/vault/ -->
<rect x="72" y="96" width="856" height="308" rx="8" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
<!-- Level 3: /business -->
<rect x="104" y="132" width="792" height="236" rx="8" fill="rgba(11,13,11,0.025)" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<!-- Level 4: /marketing -->
<rect x="136" y="168" width="728" height="164" rx="8" fill="rgba(11,13,11,0.03)" stroke="#52534e" stroke-width="1"/>
<!-- Level 5: /project — innermost, coral focal -->
<rect x="168" y="204" width="664" height="92" rx="8" fill="rgba(247,89,31,0.06)" stroke="#f7591f" stroke-width="1"/>
<!-- Level labels on paper-colored masks -->
<rect x="56" y="52" width="188" height="16" fill="#f5f4ed"/>
<text x="64" y="64" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/.claude/ (global)</text>
<rect x="88" y="88" width="148" height="16" fill="#f5f4ed"/>
<text x="96" y="100" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">~/vault/ (notes)</text>
<rect x="120" y="124" width="96" height="16" fill="#f5f4ed"/>
<text x="128" y="136" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/business</text>
<rect x="152" y="160" width="108" height="16" fill="#f5f4ed"/>
<text x="160" y="172" fill="#0b0d0b" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">/marketing</text>
<rect x="184" y="196" width="88" height="16" fill="#f5f4ed"/>
<text x="192" y="208" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em" font-weight="600">/project</text>
<!-- File-icon glyphs (simple rect with folded corner) inside each level -->
<!-- Glyph in level 1 (bottom-right area of outer ring) -->
<g transform="translate(908, 408)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.35)" stroke-width="1"/>
</g>
<!-- Glyph in level 2 ring -->
<g transform="translate(876, 372)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1"/>
</g>
<!-- Glyph in level 3 ring -->
<g transform="translate(844, 336)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="rgba(11,13,11,0.50)" stroke-width="1"/>
</g>
<!-- Glyph in level 4 ring -->
<g transform="translate(812, 300)">
<path d="M0 0 L16 0 L20 4 L20 20 L0 20 Z" fill="#f5f4ed" stroke="#52534e" stroke-width="1"/>
<path d="M16 0 L16 4 L20 4" fill="none" stroke="#52534e" stroke-width="1"/>
</g>
<!-- Innermost label — human readable -->
<text x="500" y="248" fill="#0b0d0b" font-size="16" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">CLAUDE.md</text>
<text x="500" y="272" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">inherits every level above</text>
<!-- Annotation top-right: italic callout with curved arrow -->
<text x="904" y="36" fill="#0b0d0b" font-size="14" font-style="italic" font-family="'Instrument Serif', serif" text-anchor="end">no imports, no configuration</text>
<path d="M 820 44 Q 700 84 520 216" fill="none" stroke="rgba(11,13,11,0.40)" stroke-width="1" stroke-dasharray="4,3"/>
<circle cx="520" cy="216" r="2" fill="#0b0d0b"/>
<!-- Annotation bottom-left: italic callout -->
<text x="40" y="484" fill="#52534e" font-size="14" font-style="italic" font-family="'Instrument Serif', serif">structure IS the index</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content pyramid · what compounds</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Pyramid · Diagram Design</p>
<h1>Content pyramid · what compounds</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Left axis: direction of rarity -->
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<polygon points="96,112 104,112 100,100" fill="rgba(241,239,231,0.45)"/>
<text x="80" y="216" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
<!-- Base layer: Short posts -->
<polygon points="240,280 760,280 820,344 180,344" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="500" y="308" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
<text x="500" y="324" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
<text x="836" y="316" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
<!-- L3: Essays -->
<polygon points="300,216 700,216 760,280 240,280" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="500" y="244" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
<text x="500" y="260" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 8001,500 words</text>
<text x="776" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
<!-- L2: Long-form guides -->
<polygon points="360,152 640,152 700,216 300,216" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.12)" stroke-width="1"/>
<text x="500" y="180" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
<text x="500" y="196" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
<text x="716" y="188" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
<polygon points="500,76 640,152 360,152" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
<text x="500" y="120" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
<text x="500" y="136" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 35 years</text>
<text x="656" y="112" fill="#ff6a30" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
<!-- Footnote under pyramid -->
<text x="500" y="384" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
<!-- Legend -->
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="452" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="464" width="16" height="12" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
<text x="64" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
<rect x="280" y="464" width="16" height="12" fill="rgba(241,239,231,0.04)" stroke="rgba(241,239,231,0.25)" stroke-width="1"/>
<text x="304" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
<text x="560" y="474" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content pyramid · what compounds</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Pyramid · Diagram Design</p>
<h1>Content pyramid · what compounds</h1>
<p class="subtitle">Four layers of output, ordered by how rarely they ship and how far they travel. The base keeps you present. The apex defines the body of work — and it's the only layer anyone quotes back a decade later.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Left axis: direction of rarity -->
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<polygon points="96,112 104,112 100,100" fill="rgba(11,13,11,0.45)"/>
<text x="80" y="216" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
<!-- Base layer: Short posts -->
<polygon points="240,280 760,280 820,344 180,344" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="308" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
<text x="500" y="324" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
<text x="836" y="316" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
<!-- L3: Essays -->
<polygon points="300,216 700,216 760,280 240,280" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="244" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
<text x="500" y="260" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 8001,500 words</text>
<text x="776" y="252" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
<!-- L2: Long-form guides -->
<polygon points="360,152 640,152 700,216 300,216" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="180" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
<text x="500" y="196" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
<text x="716" y="188" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
<polygon points="500,76 640,152 360,152" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="500" y="120" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
<text x="500" y="136" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 35 years</text>
<text x="656" y="112" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
<!-- Footnote under pyramid -->
<text x="500" y="384" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
<!-- Legend -->
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="452" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="464" width="16" height="12" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="64" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
<rect x="280" y="464" width="16" height="12" fill="#efeee5" stroke="rgba(11,13,11,0.25)" stroke-width="1"/>
<text x="304" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
<text x="560" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>One coral layer, on purpose</h3></div>
<p>A five-colour pyramid is a children's diagram. Reserving coral for the apex makes the whole structure actually say something: this is the rarest thing you'll make, and it's the one the rest of the pyramid is feeding.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Width tells the truth</h3></div>
<ul><li>~240 short posts a year</li><li>~48 essays</li><li>~4 long-form guides</li><li>1 flagship every 35 years</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Pyramids only work for hierarchy</h3></div>
<p>If your four categories don't have a rarity order — if a bullet list would communicate it — use bullets. Pyramids promise you're trading volume for value as you climb.</p>
</div>
</div>
<div class="footer">
<span>content pyramid · what compounds</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content pyramid · what compounds</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Pyramid · Diagram Design</p>
<h1>Content pyramid · what compounds</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Left axis: direction of rarity -->
<line x1="100" y1="112" x2="100" y2="320" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<polygon points="96,112 104,112 100,100" fill="rgba(11,13,11,0.45)"/>
<text x="80" y="216" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="middle" transform="rotate(-90 80 216)">RARER · FEWER · COMPOUNDS ↑</text>
<!-- Base layer: Short posts -->
<polygon points="240,280 760,280 820,344 180,344" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="308" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Short posts</text>
<text x="500" y="324" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">daily · ~200 words</text>
<text x="836" y="316" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~240/yr</text>
<!-- L3: Essays -->
<polygon points="300,216 700,216 760,280 240,280" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="244" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Essays</text>
<text x="500" y="260" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">weekly · 8001,500 words</text>
<text x="776" y="252" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~48/yr</text>
<!-- L2: Long-form guides -->
<polygon points="360,152 640,152 700,216 300,216" fill="#efeee5" stroke="rgba(11,13,11,0.12)" stroke-width="1"/>
<text x="500" y="180" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Long-form guides</text>
<text x="500" y="196" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">quarterly · 4,000+ words</text>
<text x="716" y="188" fill="#65655c" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">~4/yr</text>
<!-- Apex: Flagship book — CORAL FOCAL (triangle, pointed) -->
<polygon points="500,76 640,152 360,152" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="500" y="120" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Flagship book</text>
<text x="500" y="136" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">every 35 years</text>
<text x="656" y="112" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.08em">the apex</text>
<!-- Footnote under pyramid -->
<text x="500" y="384" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" text-anchor="middle" font-style="italic">The base funds the apex. The apex defines the base.</text>
<!-- Legend -->
<line x1="40" y1="436" x2="960" y2="436" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="452" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="464" width="16" height="12" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="64" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Apex — rarest, highest leverage</text>
<rect x="280" y="464" width="16" height="12" fill="#efeee5" stroke="rgba(11,13,11,0.25)" stroke-width="1"/>
<text x="304" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Supporting layer — the volume work</text>
<text x="560" y="474" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Layer width is honest: narrower = rarer shipping cadence.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content ideas · Impact × Effort</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Quadrant · Diagram Design</p>
<h1>Content ideas · Impact × Effort</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
<rect x="120" y="80" width="380" height="170" fill="rgba(255,106,48,0.03)"/>
<!-- Axis cross -->
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(241,239,231,0.45)" stroke-width="1"/>
<!-- Axis end labels -->
<text x="880" y="266" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
<text x="120" y="266" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
<text x="512" y="80" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
<text x="512" y="432" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
<!-- Quadrant corner labels -->
<text x="140" y="104" fill="#ff6a30" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
<text x="860" y="104" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
<text x="140" y="412" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
<text x="860" y="412" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
<!-- Items: TL (Do First) -->
<!-- coral focal -->
<circle cx="220" cy="140" r="6" fill="#ff6a30"/>
<text x="232" y="144" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
<circle cx="320" cy="200" r="4" fill="#f1efe7"/>
<text x="332" y="204" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
<!-- Items: TR (Major Projects) -->
<circle cx="620" cy="140" r="4" fill="#f1efe7"/>
<text x="632" y="144" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
<circle cx="760" cy="180" r="4" fill="#f1efe7"/>
<text x="772" y="184" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">New publication</text>
<!-- Items: BL (Quick Wins) -->
<circle cx="260" cy="320" r="4" fill="#f1efe7"/>
<text x="272" y="324" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
<circle cx="360" cy="380" r="4" fill="#f1efe7"/>
<text x="372" y="384" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
<!-- Items: BR (Avoid) -->
<circle cx="640" cy="380" r="4" fill="#f1efe7"/>
<text x="652" y="384" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
<circle cx="780" cy="320" r="4" fill="#f1efe7"/>
<text x="792" y="324" fill="#a8a69d" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
<!-- Legend -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="#ff6a30"/>
<text x="68" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
<circle cx="192" cy="488" r="4" fill="#f1efe7"/>
<text x="208" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
<text x="336" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content ideas · Impact × Effort</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Quadrant · Diagram Design</p>
<h1>Content ideas · Impact × Effort</h1>
<p class="subtitle">Eight candidate projects mapped by the pay-off they'd generate against the cost to ship. Position is the signal — color is reserved for the one item worth starting tomorrow.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
<rect x="120" y="80" width="380" height="170" fill="rgba(247,89,31,0.03)"/>
<!-- Axis cross -->
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<!-- Axis end labels -->
<text x="880" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
<text x="120" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
<text x="512" y="80" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
<text x="512" y="432" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
<!-- Quadrant corner labels -->
<text x="140" y="104" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
<text x="860" y="104" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
<text x="140" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
<text x="860" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
<!-- Items: TL (Do First) -->
<!-- coral focal -->
<circle cx="220" cy="140" r="6" fill="#f7591f"/>
<text x="232" y="144" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
<circle cx="320" cy="200" r="4" fill="#0b0d0b"/>
<text x="332" y="204" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
<!-- Items: TR (Major Projects) -->
<circle cx="620" cy="140" r="4" fill="#0b0d0b"/>
<text x="632" y="144" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
<circle cx="760" cy="180" r="4" fill="#0b0d0b"/>
<text x="772" y="184" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">New publication</text>
<!-- Items: BL (Quick Wins) -->
<circle cx="260" cy="320" r="4" fill="#0b0d0b"/>
<text x="272" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
<circle cx="360" cy="380" r="4" fill="#0b0d0b"/>
<text x="372" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
<!-- Items: BR (Avoid) -->
<circle cx="640" cy="380" r="4" fill="#0b0d0b"/>
<text x="652" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
<circle cx="780" cy="320" r="4" fill="#0b0d0b"/>
<text x="792" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
<!-- Legend -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="#f7591f"/>
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
<circle cx="192" cy="488" r="4" fill="#0b0d0b"/>
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
<text x="336" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>One coral dot, on purpose</h3></div>
<p>A 2×2 with four coloured quadrants is a poster, not a decision tool. Reserving coral for the single item you commit to tomorrow makes the matrix actually prioritise.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Axes labelled at ends</h3></div>
<ul><li>HIGH / LOW at the extremes</li><li>Arrows show direction</li><li>No labels at midpoints</li><li>The cross is 1px ink, not a box</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Empty BR isn't wasted</h3></div>
<p>Two items in "Avoid" is informative — it says the team considered them and rejected them. A blank quadrant would leave you wondering.</p>
</div>
</div>
<div class="footer">
<span>content ideas · impact × effort</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Content ideas · Impact × Effort</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Quadrant · Diagram Design</p>
<h1>Content ideas · Impact × Effort</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Quadrant backgrounds (very subtle, only coral on DO FIRST) -->
<rect x="120" y="80" width="380" height="170" fill="rgba(247,89,31,0.03)"/>
<!-- Axis cross -->
<line x1="120" y1="250" x2="880" y2="250" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<line x1="500" y1="80" x2="500" y2="420" stroke="rgba(11,13,11,0.45)" stroke-width="1"/>
<!-- Axis end labels -->
<text x="880" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">HIGH EFFORT →</text>
<text x="120" y="266" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">← LOW EFFORT</text>
<text x="512" y="80" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↑ HIGH IMPACT</text>
<text x="512" y="432" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">↓ LOW IMPACT</text>
<!-- Quadrant corner labels -->
<text x="140" y="104" fill="#f7591f" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em" font-weight="600">DO FIRST</text>
<text x="860" y="104" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">MAJOR PROJECTS</text>
<text x="140" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">QUICK WINS</text>
<text x="860" y="412" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.18em">AVOID</text>
<!-- Items: TL (Do First) -->
<!-- coral focal -->
<circle cx="220" cy="140" r="6" fill="#f7591f"/>
<text x="232" y="144" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif">diagram-design</text>
<circle cx="320" cy="200" r="4" fill="#0b0d0b"/>
<text x="332" y="204" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update changelog</text>
<!-- Items: TR (Major Projects) -->
<circle cx="620" cy="140" r="4" fill="#0b0d0b"/>
<text x="632" y="144" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Design v4 refresh</text>
<circle cx="760" cy="180" r="4" fill="#0b0d0b"/>
<text x="772" y="184" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">New publication</text>
<!-- Items: BL (Quick Wins) -->
<circle cx="260" cy="320" r="4" fill="#0b0d0b"/>
<text x="272" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Fix footer link</text>
<circle cx="360" cy="380" r="4" fill="#0b0d0b"/>
<text x="372" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Update OG tags</text>
<!-- Items: BR (Avoid) -->
<circle cx="640" cy="380" r="4" fill="#0b0d0b"/>
<text x="652" y="384" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Rewrite build pipeline</text>
<circle cx="780" cy="320" r="4" fill="#0b0d0b"/>
<text x="792" y="324" fill="#52534e" font-size="11" font-family="'Geist', sans-serif">Port to Nuxt</text>
<!-- Legend -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="#f7591f"/>
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Start tomorrow</text>
<circle cx="192" cy="488" r="4" fill="#0b0d0b"/>
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Candidate project</text>
<text x="336" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Position is the signal. Colour is reserved for the single action item.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,220 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article request · Sequence</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Sequence · Diagram Design</p>
<h1>Article request, cold cache</h1>
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Dot grid background -->
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#5ba8eb"/>
</marker>
</defs>
<!-- Background: paper + dot grid -->
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- =================================================================
LIFELINES — dashed vertical lines from each actor, behind everything.
Actor lifeline x-coords: 128, 352, 584, 800
================================================================= -->
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<!-- =================================================================
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
Drawn before message arrows so arrows land on their edges.
================================================================= -->
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
<rect x="348" y="180" width="8" height="232" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
<!-- Astro activation: called at y=232, returns at y=352 -->
<rect x="580" y="236" width="8" height="120" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
<!-- =================================================================
MESSAGE ARROWS — time flows top→down.
Draw before labels so label masks cover the line.
================================================================= -->
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
<line x1="128" y1="176" x2="352" y2="176" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
<line x1="352" y1="232" x2="580" y2="232" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
<line x1="580" y1="352" x2="356" y2="352" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- M5: Cloudflare → Reader (primary success · coral) -->
<line x1="348" y1="408" x2="128" y2="408" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
<line x1="128" y1="464" x2="800" y2="464" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- =================================================================
MESSAGE LABELS — each with an opaque paper-colored mask.
================================================================= -->
<!-- M1 label -->
<rect x="188" y="160" width="104" height="12" rx="2" fill="#1c1a17"/>
<text x="240" y="170" fill="#5ba8eb" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
<!-- M2 label -->
<rect x="416" y="216" width="100" height="12" rx="2" fill="#1c1a17"/>
<text x="466" y="226" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
<!-- M3 label (to the right of the self-loop) -->
<rect x="632" y="292" width="72" height="12" rx="2" fill="#1c1a17"/>
<text x="668" y="302" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
<!-- M4 label -->
<rect x="420" y="336" width="96" height="12" rx="2" fill="#1c1a17"/>
<text x="468" y="346" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
<!-- M5 label (coral · primary response) -->
<rect x="192" y="392" width="96" height="12" rx="2" fill="#1c1a17"/>
<text x="240" y="402" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
<rect x="648" y="448" width="96" height="12" rx="2" fill="#1c1a17"/>
<text x="696" y="458" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
<!-- =================================================================
ACTOR BOXES — drawn after arrows/labels.
Each actor: 144160 wide × 56 tall. Centers: 128, 352, 584, 800.
================================================================= -->
<!-- Actor 1: Reader (external / soft) -->
<rect x="56" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(168,166,157,0.10)" stroke="#8e8c83" stroke-width="1"/>
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(142,140,131,0.40)" stroke-width="0.8"/>
<text x="78" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="128" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="128" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Actor 2: Cloudflare (cloud / muted) -->
<rect x="280" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(241,239,231,0.03)" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
<text x="304" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="352" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="352" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Actor 3: Astro Origin (focal / coral) -->
<rect x="504" y="72" width="160" height="56" rx="6" fill="#1c1a17"/>
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
<text x="528" y="89" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="584" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="584" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Actor 4: Analytics (optional / dashed) -->
<rect x="728" y="72" width="144" height="56" rx="6" fill="#1c1a17"/>
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(241,239,231,0.02)" stroke="rgba(241,239,231,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.22)" stroke-width="0.8"/>
<text x="750" y="89" fill="#8e8c83" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
<text x="800" y="104" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
<text x="800" y="119" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
<!-- =================================================================
LEGEND — horizontal strip at the bottom.
Separator at y=504, eyebrow at y=520, items at y=540548.
================================================================= -->
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="56" y="520" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<!-- Item 1: Actor swatch (coral focal) -->
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<text x="76" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
<!-- Item 2: Activation bar swatch -->
<rect x="188" y="536" width="4" height="18" fill="rgba(241,239,231,0.06)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="200" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
<!-- Item 3: Request arrow (link-blue) -->
<line x1="308" y1="546" x2="336" y2="546" stroke="#5ba8eb" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="344" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<!-- Item 4: Return / async (muted dashed) -->
<line x1="476" y1="546" x2="504" y2="546" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<text x="512" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
<!-- Item 5: Primary response (coral) -->
<line x1="652" y1="546" x2="680" y2="546" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="688" y="548" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article Request · Sequence</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-paper-2: #efeee5;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-soft: #65655c;
--color-rule: rgba(11,13,11,0.12);
--color-rule-solid: rgba(135,139,134,0.25);
--color-accent: #f7591f;
--color-accent-tint: rgba(247,89,31,0.08);
--color-link: #1a70c7;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', 'Times New Roman', serif;
--font-mono: 'Geist Mono', ui-monospace, Menlo, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
min-height: 100vh;
padding: 3rem 2rem;
color: var(--color-ink);
}
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.75rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--color-ink);
margin-bottom: 0.5rem;
}
.subtitle {
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.55;
color: var(--color-muted);
max-width: 58ch;
}
.diagram-container {
background: var(--color-paper-2);
border-radius: 8px;
border: 1px solid var(--color-rule);
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 900px; display: block; }
.cards {
display: grid;
grid-template-columns: 1.1fr 1fr 0.9fr;
gap: 1rem;
margin-top: 1.5rem;
}
@media (max-width: 820px) {
.cards { grid-template-columns: 1fr; }
}
.card {
background: #ffffff;
border-radius: 6px;
border: 1px solid var(--color-rule);
padding: 1.25rem;
}
.card .eyebrow {
font-family: var(--font-mono);
font-size: 0.5rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
.card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.875rem;
padding-bottom: 0.875rem;
border-bottom: 1px solid rgba(11,13,11,0.08);
}
.card-dot {
width: 7px; height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.card-dot.ink { background: var(--color-ink); }
.card-dot.muted { background: var(--color-muted); }
.card-dot.coral { background: var(--color-accent); }
.card-dot.link { background: var(--color-link); }
.card h3 {
font-family: var(--font-sans);
font-size: 0.875rem;
font-weight: 600;
color: var(--color-ink);
letter-spacing: -0.005em;
}
.card p {
color: var(--color-muted);
font-size: 0.8125rem;
line-height: 1.55;
}
.card ul {
list-style: none;
color: var(--color-muted);
font-size: 0.8125rem;
line-height: 1.55;
}
.card li {
margin-bottom: 0.3rem;
padding-left: 0.875rem;
position: relative;
}
.card li::before {
content: '—';
position: absolute;
left: 0;
color: rgba(11,13,11,0.25);
font-size: 0.75rem;
}
.footer {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(11,13,11,0.10);
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.06em;
color: var(--color-soft);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<p class="header-eyebrow">Sequence · Diagram Design</p>
<h1>Article request, cold cache</h1>
<p class="subtitle">How littlemight.com serves a reader when the requested slug isn't already sitting in Cloudflare's edge cache — origin render, beacon, and back in one round trip.</p>
</div>
<!-- Sequence Diagram -->
<div class="diagram-container">
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Dot grid background -->
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#52534e"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f7591f"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/>
</marker>
</defs>
<!-- Background: paper + dot grid -->
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- =================================================================
LIFELINES — dashed vertical lines from each actor, behind everything.
Actor lifeline x-coords: 128, 352, 584, 800
================================================================= -->
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<!-- =================================================================
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
Drawn before message arrows so arrows land on their edges.
================================================================= -->
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
<rect x="348" y="180" width="8" height="232" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<!-- Astro activation: called at y=232, returns at y=352 -->
<rect x="580" y="236" width="8" height="120" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<!-- =================================================================
MESSAGE ARROWS — time flows top→down.
Draw before labels so label masks cover the line.
================================================================= -->
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
<line x1="128" y1="176" x2="352" y2="176" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
<line x1="352" y1="232" x2="580" y2="232" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
<line x1="580" y1="352" x2="356" y2="352" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- M5: Cloudflare → Reader (primary success · coral) -->
<line x1="348" y1="408" x2="128" y2="408" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
<line x1="128" y1="464" x2="800" y2="464" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- =================================================================
MESSAGE LABELS — each with an opaque paper-colored mask.
================================================================= -->
<!-- M1 label -->
<rect x="188" y="160" width="104" height="12" rx="2" fill="#f5f4ed"/>
<text x="240" y="170" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
<!-- M2 label -->
<rect x="416" y="216" width="100" height="12" rx="2" fill="#f5f4ed"/>
<text x="466" y="226" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
<!-- M3 label (to the right of the self-loop) -->
<rect x="632" y="292" width="72" height="12" rx="2" fill="#f5f4ed"/>
<text x="668" y="302" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
<!-- M4 label -->
<rect x="420" y="336" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="468" y="346" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
<!-- M5 label (coral · primary response) -->
<rect x="192" y="392" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="240" y="402" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
<rect x="648" y="448" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="696" y="458" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
<!-- =================================================================
ACTOR BOXES — drawn after arrows/labels.
Each actor: 144160 wide × 56 tall. Centers: 128, 352, 584, 800.
================================================================= -->
<!-- Actor 1: Reader (external / soft) -->
<rect x="56" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
<text x="78" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="128" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="128" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Actor 2: Cloudflare (cloud / muted) -->
<rect x="280" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="304" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="352" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="352" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Actor 3: Astro Origin (focal / coral) -->
<rect x="504" y="72" width="160" height="56" rx="6" fill="#f5f4ed"/>
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="528" y="89" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="584" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="584" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Actor 4: Analytics (optional / dashed) -->
<rect x="728" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="750" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
<text x="800" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
<text x="800" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
<!-- =================================================================
LEGEND — horizontal strip at the bottom.
Separator at y=504, eyebrow at y=520, items at y=540548.
================================================================= -->
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="56" y="520" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<!-- Item 1: Actor swatch (coral focal) -->
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="76" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
<!-- Item 2: Activation bar swatch -->
<rect x="188" y="536" width="4" height="18" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<text x="200" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
<!-- Item 3: Request arrow (link-blue) -->
<line x1="308" y1="546" x2="336" y2="546" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="344" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<!-- Item 4: Return / async (muted dashed) -->
<line x1="476" y1="546" x2="504" y2="546" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<text x="512" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
<!-- Item 5: Primary response (coral) -->
<line x1="652" y1="546" x2="680" y2="546" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="688" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
</svg>
</div>
<!-- Summary cards -->
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header">
<span class="card-dot coral"></span>
<h3>Edge handles the hot path</h3>
</div>
<p>On subsequent reads the whole exchange collapses to M1 → M5. Cloudflare serves the cached HTML without waking the origin. The coral arrow is the only one the reader ever perceives.</p>
</div>
<div class="card">
<div class="card-header">
<span class="card-dot ink"></span>
<h3>Origin render on miss</h3>
</div>
<ul>
<li>Astro SSRs MDX on cold cache</li>
<li>Returns HTML + cache headers</li>
<li>Edge stores the result</li>
<li>Next reader skips the origin trip</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<span class="card-dot muted"></span>
<h3>Analytics is fire-and-forget</h3>
</div>
<p>The pageview beacon is dashed for a reason: the reader never waits on it, and a failed beacon never breaks the page.</p>
</div>
</div>
<!-- Footer -->
<div class="footer">
<span>littlemight.com · article request sequence</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,220 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article request · Sequence</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Sequence · Diagram Design</p>
<h1>Article request, cold cache</h1>
<svg viewBox="0 0 1000 584" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Dot grid background -->
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<!-- Arrow markers -->
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#52534e"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f7591f"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#1a70c7"/>
</marker>
</defs>
<!-- Background: paper + dot grid -->
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- =================================================================
LIFELINES — dashed vertical lines from each actor, behind everything.
Actor lifeline x-coords: 128, 352, 584, 800
================================================================= -->
<line x1="128" y1="128" x2="128" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="352" y1="128" x2="352" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="584" y1="128" x2="584" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<line x1="800" y1="128" x2="800" y2="488" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="3,3"/>
<!-- =================================================================
ACTIVATION BARS — w=8 rects on lifelines showing control duration.
Drawn before message arrows so arrows land on their edges.
================================================================= -->
<!-- Cloudflare activation: receives at y=176, responds at y=408 -->
<rect x="348" y="180" width="8" height="232" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<!-- Astro activation: called at y=232, returns at y=352 -->
<rect x="580" y="236" width="8" height="120" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<!-- =================================================================
MESSAGE ARROWS — time flows top→down.
Draw before labels so label masks cover the line.
================================================================= -->
<!-- M1: Reader → Cloudflare (HTTPS request · link-blue) -->
<line x1="128" y1="176" x2="352" y2="176" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<!-- M2: Cloudflare → Astro (cache miss · muted) -->
<line x1="352" y1="232" x2="580" y2="232" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M3: Astro self-message (render MDX · muted U-loop) -->
<path d="M 588 284 L 624 284 L 624 316 L 588 316" fill="none" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- M4: Astro → Cloudflare (return HTML · muted dashed) -->
<line x1="580" y1="352" x2="356" y2="352" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- M5: Cloudflare → Reader (primary success · coral) -->
<line x1="348" y1="408" x2="128" y2="408" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- M6: Reader → Analytics (async beacon · muted dashed) -->
<line x1="128" y1="464" x2="800" y2="464" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- =================================================================
MESSAGE LABELS — each with an opaque paper-colored mask.
================================================================= -->
<!-- M1 label -->
<rect x="188" y="160" width="104" height="12" rx="2" fill="#f5f4ed"/>
<text x="240" y="170" fill="#1a70c7" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">GET /ARTICLES/SLUG</text>
<!-- M2 label -->
<rect x="416" y="216" width="100" height="12" rx="2" fill="#f5f4ed"/>
<text x="466" y="226" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CACHE MISS · ORIGIN</text>
<!-- M3 label (to the right of the self-loop) -->
<rect x="632" y="292" width="72" height="12" rx="2" fill="#f5f4ed"/>
<text x="668" y="302" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RENDER MDX</text>
<!-- M4 label -->
<rect x="420" y="336" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="468" y="346" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · HTML + MAX-AGE</text>
<!-- M5 label (coral · primary response) -->
<rect x="192" y="392" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="240" y="402" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">200 · EDGE-CACHED</text>
<!-- M6 label (placed between Astro and Analytics lifelines, a clear gap) -->
<rect x="648" y="448" width="96" height="12" rx="2" fill="#f5f4ed"/>
<text x="696" y="458" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PAGEVIEW BEACON</text>
<!-- =================================================================
ACTOR BOXES — drawn after arrows/labels.
Each actor: 144160 wide × 56 tall. Centers: 128, 352, 584, 800.
================================================================= -->
<!-- Actor 1: Reader (external / soft) -->
<rect x="56" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="56" y="72" width="144" height="56" rx="6" fill="rgba(82,83,78,0.10)" stroke="#65655c" stroke-width="1"/>
<rect x="64" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(101,101,92,0.40)" stroke-width="0.8"/>
<text x="78" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
<text x="128" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Reader</text>
<text x="128" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Browser</text>
<!-- Actor 2: Cloudflare (cloud / muted) -->
<rect x="280" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="280" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.03)" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<rect x="288" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="304" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EDGE</text>
<text x="352" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
<text x="352" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Pages · cache</text>
<!-- Actor 3: Astro Origin (focal / coral) -->
<rect x="504" y="72" width="160" height="56" rx="6" fill="#f5f4ed"/>
<rect x="504" y="72" width="160" height="56" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="512" y="80" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="528" y="89" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORIG</text>
<text x="584" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Astro Origin</text>
<text x="584" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">SSR + MDX</text>
<!-- Actor 4: Analytics (optional / dashed) -->
<rect x="728" y="72" width="144" height="56" rx="6" fill="#f5f4ed"/>
<rect x="728" y="72" width="144" height="56" rx="6" fill="rgba(11,13,11,0.02)" stroke="rgba(11,13,11,0.22)" stroke-width="1" stroke-dasharray="4,3"/>
<rect x="736" y="80" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.22)" stroke-width="0.8"/>
<text x="750" y="89" fill="#65655c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ASY</text>
<text x="800" y="104" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Analytics</text>
<text x="800" y="119" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Beacon · async</text>
<!-- =================================================================
LEGEND — horizontal strip at the bottom.
Separator at y=504, eyebrow at y=520, items at y=540548.
================================================================= -->
<line x1="56" y1="504" x2="944" y2="504" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="56" y="520" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<!-- Item 1: Actor swatch (coral focal) -->
<rect x="56" y="540" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="76" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal actor</text>
<!-- Item 2: Activation bar swatch -->
<rect x="188" y="536" width="4" height="18" fill="rgba(11,13,11,0.06)" stroke="#52534e" stroke-width="0.8"/>
<text x="200" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Activation</text>
<!-- Item 3: Request arrow (link-blue) -->
<line x1="308" y1="546" x2="336" y2="546" stroke="#1a70c7" stroke-width="1.2" marker-end="url(#arrow-link)"/>
<text x="344" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">HTTP request</text>
<!-- Item 4: Return / async (muted dashed) -->
<line x1="476" y1="546" x2="504" y2="546" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<text x="512" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Return / async</text>
<!-- Item 5: Primary response (coral) -->
<line x1="652" y1="546" x2="680" y2="546" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="688" y="548" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Primary response</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article lifecycle · State machine</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">State machine · Diagram Design</p>
<h1>Article lifecycle</h1>
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Transitions (drawn first) -->
<!-- Start → Draft -->
<line x1="68" y1="200" x2="120" y2="200" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Draft → In Review -->
<line x1="280" y1="200" x2="340" y2="200" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Published (coral — happy path) -->
<line x1="500" y1="200" x2="560" y2="200" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Published → Archived (down) -->
<line x1="640" y1="240" x2="640" y2="300" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Archived → End (down) -->
<line x1="640" y1="400" x2="640" y2="432" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Draft (curved back up-and-left) -->
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- Transition labels -->
<rect x="172" y="184" width="48" height="12" rx="2" fill="#1c1a17"/>
<text x="196" y="194" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
<rect x="288" y="184" width="48" height="12" rx="2" fill="#1c1a17"/>
<text x="312" y="194" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
<rect x="500" y="184" width="60" height="12" rx="2" fill="#1c1a17"/>
<text x="530" y="194" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
<rect x="612" y="264" width="56" height="12" rx="2" fill="#1c1a17"/>
<text x="640" y="274" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
<rect x="620" y="416" width="40" height="12" rx="2" fill="#1c1a17"/>
<text x="640" y="426" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
<!-- Reject label on curved path -->
<rect x="276" y="92" width="80" height="12" rx="2" fill="#1c1a17"/>
<text x="316" y="102" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
<!-- Start dot (filled ink) -->
<circle cx="60" cy="200" r="6" fill="#f1efe7"/>
<!-- State: Draft -->
<rect x="120" y="160" width="160" height="80" rx="8" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="148" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="200" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
<text x="200" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
<!-- State: In Review -->
<rect x="340" y="160" width="160" height="80" rx="8" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="368" y="177" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="420" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
<text x="420" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
<!-- State: Published (focal coral) -->
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
<text x="588" y="177" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="208" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
<text x="640" y="224" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
<!-- State: Archived -->
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="1"/>
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(168,166,157,0.50)" stroke-width="0.8"/>
<text x="588" y="317" fill="#a8a69d" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="348" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
<text x="640" y="364" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
<text x="640" y="382" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
<!-- End ring dot -->
<circle cx="640" cy="440" r="8" fill="none" stroke="#f1efe7" stroke-width="1"/>
<circle cx="640" cy="440" r="5" fill="#f1efe7"/>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article lifecycle · state machine</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">State machine · Diagram Design</p>
<h1>Article lifecycle</h1>
<p class="subtitle">The four states a post passes through from draft to archive, with the rejection loop made explicit. Published is the state the team optimizes for — hence coral.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Transitions (drawn first) -->
<!-- Start → Draft -->
<line x1="68" y1="200" x2="120" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Draft → In Review -->
<line x1="280" y1="200" x2="340" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Published (coral — happy path) -->
<line x1="500" y1="200" x2="560" y2="200" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Published → Archived (down) -->
<line x1="640" y1="240" x2="640" y2="300" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Archived → End (down) -->
<line x1="640" y1="400" x2="640" y2="432" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Draft (curved back up-and-left) -->
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- Transition labels -->
<rect x="172" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="196" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
<rect x="288" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="312" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
<rect x="500" y="184" width="60" height="12" rx="2" fill="#f5f4ed"/>
<text x="530" y="194" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
<rect x="612" y="264" width="56" height="12" rx="2" fill="#f5f4ed"/>
<text x="640" y="274" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
<rect x="620" y="416" width="40" height="12" rx="2" fill="#f5f4ed"/>
<text x="640" y="426" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
<!-- Reject label on curved path -->
<rect x="276" y="92" width="80" height="12" rx="2" fill="#f5f4ed"/>
<text x="316" y="102" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
<!-- Start dot (filled ink) -->
<circle cx="60" cy="200" r="6" fill="#0b0d0b"/>
<!-- State: Draft -->
<rect x="120" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="148" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="200" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
<text x="200" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
<!-- State: In Review -->
<rect x="340" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="368" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="420" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
<text x="420" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
<!-- State: Published (focal coral) -->
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="588" y="177" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
<text x="640" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
<!-- State: Archived -->
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
<text x="588" y="317" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="348" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
<text x="640" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
<text x="640" y="382" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
<!-- End ring dot -->
<circle cx="640" cy="440" r="8" fill="none" stroke="#0b0d0b" stroke-width="1"/>
<circle cx="640" cy="440" r="5" fill="#0b0d0b"/>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>Published is the win state</h3></div>
<p>Everything flows toward Published. The rejection loop is dashed because it's a detour, not a failure — the article is still in-progress.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Reject is a loop, not a dead-end</h3></div>
<ul><li>Dashed to signal detour</li><li>Returns to Draft, not a new state</li><li>Review feedback is the action</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Archive keeps the URL</h3></div>
<p>Archived preserves the redirect map so inbound links survive. Only Purge removes the record entirely.</p>
</div>
</div>
<div class="footer">
<span>article lifecycle · state machine</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Article lifecycle · State machine</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">State machine · Diagram Design</p>
<h1>Article lifecycle</h1>
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Transitions (drawn first) -->
<!-- Start → Draft -->
<line x1="68" y1="200" x2="120" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Draft → In Review -->
<line x1="280" y1="200" x2="340" y2="200" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Published (coral — happy path) -->
<line x1="500" y1="200" x2="560" y2="200" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- Published → Archived (down) -->
<line x1="640" y1="240" x2="640" y2="300" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Archived → End (down) -->
<line x1="640" y1="400" x2="640" y2="432" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- In Review → Draft (curved back up-and-left) -->
<path d="M 420 160 C 420 96, 200 96, 200 160" fill="none" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- Transition labels -->
<rect x="172" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="196" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CREATE</text>
<rect x="288" y="184" width="48" height="12" rx="2" fill="#f5f4ed"/>
<text x="312" y="194" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SUBMIT</text>
<rect x="500" y="184" width="60" height="12" rx="2" fill="#f5f4ed"/>
<text x="530" y="194" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APPROVE</text>
<rect x="612" y="264" width="56" height="12" rx="2" fill="#f5f4ed"/>
<text x="640" y="274" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXPIRE</text>
<rect x="620" y="416" width="40" height="12" rx="2" fill="#f5f4ed"/>
<text x="640" y="426" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PURGE</text>
<!-- Reject label on curved path -->
<rect x="276" y="92" width="80" height="12" rx="2" fill="#f5f4ed"/>
<text x="316" y="102" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">REJECT · REVISE</text>
<!-- Start dot (filled ink) -->
<circle cx="60" cy="200" r="6" fill="#0b0d0b"/>
<!-- State: Draft -->
<rect x="120" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="128" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="148" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="200" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft</text>
<text x="200" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">unpublished</text>
<!-- State: In Review -->
<rect x="340" y="160" width="160" height="80" rx="8" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="348" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="368" y="177" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="420" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">In Review</text>
<text x="420" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">awaiting approval</text>
<!-- State: Published (focal coral) -->
<rect x="560" y="160" width="160" height="80" rx="8" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="568" y="168" width="40" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="588" y="177" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="208" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Published</text>
<text x="640" y="224" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">live on site</text>
<!-- State: Archived -->
<rect x="560" y="300" width="160" height="100" rx="8" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="1"/>
<rect x="568" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(82,83,78,0.50)" stroke-width="0.8"/>
<text x="588" y="317" fill="#52534e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STATE</text>
<text x="640" y="348" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Archived</text>
<text x="640" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">noindex · hidden</text>
<text x="640" y="382" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">redirect retained</text>
<!-- End ring dot -->
<circle cx="640" cy="440" r="8" fill="none" stroke="#0b0d0b" stroke-width="1"/>
<circle cx="640" cy="440" r="5" fill="#0b0d0b"/>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Publishing an article · Swimlane</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Swimlane · Diagram Design</p>
<h1>Publishing an article</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#a8a69d"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#ff6a30"/></marker>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Lane dividers -->
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(241,239,231,0.10)" stroke-width="1"/>
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<!-- Actor column divider -->
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(241,239,231,0.22)" stroke-width="1"/>
<!-- Lane labels -->
<text x="60" y="124" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
<text x="60" y="204" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
<text x="60" y="284" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
<text x="60" y="364" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
<!-- Arrows (drawn first) -->
<!-- 1. Draft → Open PR (within Author) -->
<line x1="300" y1="120" x2="340" y2="120" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
<line x1="460" y1="144" x2="500" y2="176" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
<line x1="570" y1="224" x2="570" y2="256" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- 4. Polish → Approve (within Editor) -->
<line x1="640" y1="280" x2="680" y2="280" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
<line x1="750" y1="304" x2="720" y2="336" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- 6. Build → Deploy (within CI/CD) -->
<line x1="760" y1="360" x2="800" y2="360" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="452" y="140" width="60" height="12" rx="2" fill="#1c1a17"/>
<text x="482" y="150" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
<rect x="548" y="228" width="52" height="12" rx="2" fill="#2a2723"/>
<text x="574" y="238" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
<rect x="712" y="308" width="80" height="12" rx="2" fill="#1c1a17"/>
<text x="752" y="318" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
<!-- Steps -->
<!-- Author: Draft MDX -->
<rect x="180" y="96" width="120" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="240" y="118" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
<text x="240" y="132" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
<!-- Author: Open PR -->
<rect x="340" y="96" width="120" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="400" y="118" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
<text x="400" y="132" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
<!-- Reviewer: Review content -->
<rect x="500" y="176" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="570" y="198" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
<text x="570" y="212" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
<!-- Editor: Polish copy -->
<rect x="500" y="256" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="570" y="278" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
<text x="570" y="292" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
<!-- Editor: Approve merge -->
<rect x="680" y="256" width="140" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="750" y="278" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
<text x="750" y="292" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
<!-- CI/CD: Build -->
<rect x="680" y="336" width="80" height="48" rx="6" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="720" y="358" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
<text x="720" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
<!-- CI/CD: Deploy (coral focal) -->
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<text x="860" y="358" fill="#f1efe7" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
<text x="860" y="372" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
<!-- Legend -->
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="444" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="460" width="14" height="10" rx="2" fill="#2a2723" stroke="#f1efe7" stroke-width="1"/>
<text x="60" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(255,106,48,0.08)" stroke="#ff6a30" stroke-width="1"/>
<text x="152" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
<line x1="268" y1="466" x2="296" y2="466" stroke="#a8a69d" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="304" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
<line x1="444" y1="466" x2="472" y2="466" stroke="#a8a69d" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
<text x="480" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
<line x1="608" y1="466" x2="636" y2="466" stroke="#ff6a30" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="644" y="468" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Publishing an article · Swimlane</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Swimlane · Diagram Design</p>
<h1>Publishing an article</h1>
<p class="subtitle">Four actors, seven steps, three handoffs. The step each person owns lives in their lane — arrows crossing lanes are where coordination happens.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Lane dividers -->
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<!-- Actor column divider -->
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<!-- Lane labels -->
<text x="60" y="124" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
<text x="60" y="204" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
<text x="60" y="284" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
<text x="60" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
<!-- Arrows (drawn first) -->
<!-- 1. Draft → Open PR (within Author) -->
<line x1="300" y1="120" x2="340" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
<line x1="460" y1="144" x2="500" y2="176" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
<line x1="570" y1="224" x2="570" y2="256" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- 4. Polish → Approve (within Editor) -->
<line x1="640" y1="280" x2="680" y2="280" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
<line x1="750" y1="304" x2="720" y2="336" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- 6. Build → Deploy (within CI/CD) -->
<line x1="760" y1="360" x2="800" y2="360" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="452" y="140" width="60" height="12" rx="2" fill="#f5f4ed"/>
<text x="482" y="150" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
<rect x="548" y="228" width="52" height="12" rx="2" fill="#efeee5"/>
<text x="574" y="238" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
<rect x="712" y="308" width="80" height="12" rx="2" fill="#f5f4ed"/>
<text x="752" y="318" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
<!-- Steps -->
<!-- Author: Draft MDX -->
<rect x="180" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="240" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
<text x="240" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
<!-- Author: Open PR -->
<rect x="340" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="400" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
<text x="400" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
<!-- Reviewer: Review content -->
<rect x="500" y="176" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="570" y="198" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
<text x="570" y="212" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
<!-- Editor: Polish copy -->
<rect x="500" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="570" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
<text x="570" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
<!-- Editor: Approve merge -->
<rect x="680" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="750" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
<text x="750" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
<!-- CI/CD: Build -->
<rect x="680" y="336" width="80" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="720" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
<text x="720" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
<!-- CI/CD: Deploy (coral focal) -->
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="860" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
<text x="860" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
<!-- Legend -->
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="444" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="460" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="60" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="152" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
<line x1="268" y1="466" x2="296" y2="466" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="304" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
<line x1="444" y1="466" x2="472" y2="466" stroke="#52534e" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
<text x="480" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
<line x1="608" y1="466" x2="636" y2="466" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="644" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>The deploy trigger is the risky edge</h3></div>
<p>Every other step lives inside one person's head. The Editor→CI/CD handoff is where automation takes over and humans lose the ability to undo — hence coral.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>One owner per step</h3></div>
<ul><li>No step crosses two lanes</li><li>Handoffs are arrows, not shared steps</li><li>Dashed arrow = revision detour</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Uneven step counts are fine</h3></div>
<p>The Reviewer lane has one step; CI/CD has two. Lanes don't need to match — they exist to make ownership unambiguous.</p>
</div>
</div>
<div class="footer">
<span>publishing an article · swimlane</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Publishing an article · Swimlane</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Swimlane · Diagram Design</p>
<h1>Publishing an article</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#52534e"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f7591f"/></marker>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Lane dividers -->
<line x1="40" y1="80" x2="960" y2="80" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<line x1="40" y1="160" x2="960" y2="160" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="240" x2="960" y2="240" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="320" x2="960" y2="320" stroke="rgba(11,13,11,0.10)" stroke-width="1"/>
<line x1="40" y1="400" x2="960" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<!-- Actor column divider -->
<line x1="160" y1="80" x2="160" y2="400" stroke="rgba(11,13,11,0.22)" stroke-width="1"/>
<!-- Lane labels -->
<text x="60" y="124" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">AUTHOR</text>
<text x="60" y="204" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">REVIEWER</text>
<text x="60" y="284" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">EDITOR</text>
<text x="60" y="364" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" letter-spacing="0.18em">CI / CD</text>
<!-- Arrows (drawn first) -->
<!-- 1. Draft → Open PR (within Author) -->
<line x1="300" y1="120" x2="340" y2="120" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 2. Open PR → Review (Author → Reviewer handoff, diagonal down-right) -->
<line x1="460" y1="144" x2="500" y2="176" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 3. Review → Polish (Reviewer → Editor, vertical down) -->
<line x1="570" y1="224" x2="570" y2="256" stroke="#52534e" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- 4. Polish → Approve (within Editor) -->
<line x1="640" y1="280" x2="680" y2="280" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- 5. Approve → Build (Editor → CI/CD, coral handoff) -->
<line x1="750" y1="304" x2="720" y2="336" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<!-- 6. Build → Deploy (within CI/CD) -->
<line x1="760" y1="360" x2="800" y2="360" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<!-- Arrow labels -->
<rect x="452" y="140" width="60" height="12" rx="2" fill="#f5f4ed"/>
<text x="482" y="150" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">HANDOFF</text>
<rect x="548" y="228" width="52" height="12" rx="2" fill="#efeee5"/>
<text x="574" y="238" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">REVISE</text>
<rect x="712" y="308" width="80" height="12" rx="2" fill="#f5f4ed"/>
<text x="752" y="318" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">DEPLOY TRIGGER</text>
<!-- Steps -->
<!-- Author: Draft MDX -->
<rect x="180" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="240" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Draft MDX</text>
<text x="240" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">src/content/…</text>
<!-- Author: Open PR -->
<rect x="340" y="96" width="120" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="400" y="118" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Open PR</text>
<text x="400" y="132" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">gh pr create</text>
<!-- Reviewer: Review content -->
<rect x="500" y="176" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="570" y="198" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Review content</text>
<text x="570" y="212" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">fact-check · voice</text>
<!-- Editor: Polish copy -->
<rect x="500" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="570" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Polish copy</text>
<text x="570" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">style · line edits</text>
<!-- Editor: Approve merge -->
<rect x="680" y="256" width="140" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="750" y="278" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Approve merge</text>
<text x="750" y="292" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">squash · main</text>
<!-- CI/CD: Build -->
<rect x="680" y="336" width="80" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="720" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
<text x="720" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">astro build</text>
<!-- CI/CD: Deploy (coral focal) -->
<rect x="800" y="336" width="120" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="860" y="358" fill="#0b0d0b" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Deploy</text>
<text x="860" y="372" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">cloudflare pages</text>
<!-- Legend -->
<line x1="40" y1="428" x2="960" y2="428" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="444" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="460" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="60" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Step</text>
<rect x="132" y="460" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="152" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Focal outcome</text>
<line x1="268" y1="466" x2="296" y2="466" stroke="#52534e" stroke-width="1.2" marker-end="url(#arrow)"/>
<text x="304" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Within-lane step</text>
<line x1="444" y1="466" x2="472" y2="466" stroke="#52534e" stroke-width="1.2" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
<text x="480" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Revision loop</text>
<line x1="608" y1="466" x2="636" y2="466" stroke="#f7591f" stroke-width="1.4" marker-end="url(#arrow-accent)"/>
<text x="644" y="468" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Critical handoff</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight milestones · Timeline</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Timeline · Diagram Design</p>
<h1>littlemight, fourteen months</h1>
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Year markers (background) -->
<text x="60" y="340" fill="rgba(241,239,231,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
<text x="700" y="340" fill="rgba(241,239,231,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
<!-- Baseline -->
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(150,146,138,0.45)" stroke-width="1"/>
<!-- Year boundary tick (between 2025 and 2026) -->
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
<text x="680" y="260" fill="#8e8c83" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
<!-- Start / end caps -->
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(241,239,231,0.20)" stroke-width="1"/>
<!-- Event 1: FEB 2025 · First post (below) -->
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<circle cx="100" cy="240" r="4" fill="#f1efe7"/>
<text x="100" y="312" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
<text x="100" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
<!-- Event 2: APR 2025 · Design v1 (above) -->
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<circle cx="240" cy="240" r="4" fill="#f1efe7"/>
<text x="240" y="156" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
<text x="240" y="172" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
<!-- Event 3: SEP 2025 · Design v2 (below) -->
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(241,239,231,0.30)" stroke-width="1"/>
<circle cx="500" cy="240" r="4" fill="#f1efe7"/>
<text x="500" y="312" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
<text x="500" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
<text x="500" y="344" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
<line x1="740" y1="160" x2="740" y2="240" stroke="#ff6a30" stroke-width="1"/>
<circle cx="740" cy="240" r="6" fill="#ff6a30"/>
<text x="740" y="128" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
<text x="740" y="148" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
<text x="740" y="164" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
<line x1="900" y1="240" x2="900" y2="296" stroke="#ff6a30" stroke-width="1"/>
<circle cx="900" cy="240" r="6" fill="#ff6a30"/>
<text x="900" y="312" fill="#ff6a30" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
<text x="900" y="328" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
<text x="900" y="344" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
<!-- Legend -->
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="392" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="408" r="4" fill="#f1efe7"/>
<text x="68" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
<circle cx="148" cy="408" r="6" fill="#ff6a30"/>
<text x="164" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
<text x="296" y="412" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight milestones · Timeline</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Timeline · Diagram Design</p>
<h1>littlemight, fourteen months</h1>
<p class="subtitle">Five design moments from the first post to the current diagram-design overhaul. Spacing is proportional to calendar time — the five-month gap between v1 and v2 is genuinely wider on the page.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Year markers (background) -->
<text x="60" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
<text x="700" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
<!-- Baseline -->
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(135,139,134,0.45)" stroke-width="1"/>
<!-- Year boundary tick (between 2025 and 2026) -->
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<text x="680" y="260" fill="#65655c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
<!-- Start / end caps -->
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<!-- Event 1: FEB 2025 · First post (below) -->
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="100" cy="240" r="4" fill="#0b0d0b"/>
<text x="100" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
<text x="100" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
<!-- Event 2: APR 2025 · Design v1 (above) -->
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="240" cy="240" r="4" fill="#0b0d0b"/>
<text x="240" y="156" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
<text x="240" y="172" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
<!-- Event 3: SEP 2025 · Design v2 (below) -->
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="500" cy="240" r="4" fill="#0b0d0b"/>
<text x="500" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
<text x="500" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
<text x="500" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
<line x1="740" y1="160" x2="740" y2="240" stroke="#f7591f" stroke-width="1"/>
<circle cx="740" cy="240" r="6" fill="#f7591f"/>
<text x="740" y="128" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
<text x="740" y="148" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
<text x="740" y="164" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
<line x1="900" y1="240" x2="900" y2="296" stroke="#f7591f" stroke-width="1"/>
<circle cx="900" cy="240" r="6" fill="#f7591f"/>
<text x="900" y="312" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
<text x="900" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
<text x="900" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
<!-- Legend -->
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="392" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="408" r="4" fill="#0b0d0b"/>
<text x="68" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
<circle cx="148" cy="408" r="6" fill="#f7591f"/>
<text x="164" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
<text x="296" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>v3 set the complexity budget</h3></div>
<p>January's design overhaul is the pivot — the rule that no diagram exceeds nine components comes from there, and it's why this skill exists in the form it does.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Honest spacing</h3></div>
<ul><li>5 months between v1 and v2</li><li>4 months between v2 and v3</li><li>3 months between v3 and now</li><li>Cadence is tightening, not gaming the axis</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Alternate label placement</h3></div>
<p>Above / below flipping prevents label collision without forcing a second row. Five events on one baseline stays legible.</p>
</div>
</div>
<div class="footer">
<span>littlemight · design lineage</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>littlemight milestones · Timeline</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Timeline · Diagram Design</p>
<h1>littlemight, fourteen months</h1>
<svg viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Year markers (background) -->
<text x="60" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2025</text>
<text x="700" y="340" fill="rgba(11,13,11,0.06)" font-size="72" font-weight="600" font-family="'Geist Mono', monospace">2026</text>
<!-- Baseline -->
<line x1="80" y1="240" x2="920" y2="240" stroke="rgba(135,139,134,0.45)" stroke-width="1"/>
<!-- Year boundary tick (between 2025 and 2026) -->
<line x1="680" y1="232" x2="680" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<text x="680" y="260" fill="#65655c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN '26</text>
<!-- Start / end caps -->
<line x1="80" y1="232" x2="80" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<line x1="920" y1="232" x2="920" y2="248" stroke="rgba(11,13,11,0.20)" stroke-width="1"/>
<!-- Event 1: FEB 2025 · First post (below) -->
<line x1="100" y1="240" x2="100" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="100" cy="240" r="4" fill="#0b0d0b"/>
<text x="100" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">FEB 2025</text>
<text x="100" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First post</text>
<!-- Event 2: APR 2025 · Design v1 (above) -->
<line x1="240" y1="184" x2="240" y2="240" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="240" cy="240" r="4" fill="#0b0d0b"/>
<text x="240" y="156" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2025</text>
<text x="240" y="172" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v1</text>
<!-- Event 3: SEP 2025 · Design v2 (below) -->
<line x1="500" y1="240" x2="500" y2="296" stroke="rgba(11,13,11,0.30)" stroke-width="1"/>
<circle cx="500" cy="240" r="4" fill="#0b0d0b"/>
<text x="500" y="312" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">SEP 2025</text>
<text x="500" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v2</text>
<text x="500" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">typography pass</text>
<!-- Event 4: JAN 2026 · Design v3 (above, coral major) -->
<line x1="740" y1="160" x2="740" y2="240" stroke="#f7591f" stroke-width="1"/>
<circle cx="740" cy="240" r="6" fill="#f7591f"/>
<text x="740" y="128" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">JAN 2026</text>
<text x="740" y="148" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design v3</text>
<text x="740" y="164" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">complexity budget</text>
<!-- Event 5: APR 2026 · now · Diagram Design skill (below, coral) -->
<line x1="900" y1="240" x2="900" y2="296" stroke="#f7591f" stroke-width="1"/>
<circle cx="900" cy="240" r="6" fill="#f7591f"/>
<text x="900" y="312" fill="#f7591f" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">APR 2026 · NOW</text>
<text x="900" y="328" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">diagram-design</text>
<text x="900" y="344" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">eight diagram types</text>
<!-- Legend -->
<line x1="40" y1="376" x2="960" y2="376" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="392" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="408" r="4" fill="#0b0d0b"/>
<text x="68" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Event</text>
<circle cx="148" cy="408" r="6" fill="#f7591f"/>
<text x="164" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Major milestone</text>
<text x="296" y="412" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Spacing is proportional to real elapsed time.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code skill taxonomy · Tree</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Tree · Diagram Design</p>
<h1>Claude Code skill taxonomy</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Tier tags (left margin) -->
<text x="40" y="108" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
<text x="40" y="224" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
<text x="40" y="324" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
<!-- Connectors drawn first (behind nodes) -->
<!-- Root → Tier 1 bus -->
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
<path d="M 500 168 L 500 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#a8a69d" stroke-width="1"/>
<!-- Design → leaves -->
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
<!-- Engineering → leaves -->
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
<!-- Research → single leaf -->
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#a8a69d" stroke-width="1"/>
<!-- Root node: Skills (coral focal) -->
<rect x="420" y="80" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(255,106,48,0.50)" stroke-width="0.8"/>
<text x="444" y="97" fill="#ff6a30" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
<text x="500" y="118" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
<!-- Tier 1: Design -->
<rect x="140" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="140" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="162" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="220" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
<text x="220" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
<!-- Tier 1: Engineering -->
<rect x="420" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="420" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="442" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="500" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
<text x="500" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
<!-- Tier 1: Research -->
<rect x="700" y="208" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="700" y="208" width="160" height="48" rx="6" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(241,239,231,0.40)" stroke-width="0.8"/>
<text x="722" y="225" fill="#f1efe7" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="780" y="240" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
<text x="780" y="252" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
<!-- Tier 2: polish -->
<rect x="60" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="140" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
<text x="140" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
<!-- Tier 2: critique -->
<rect x="220" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="300" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
<text x="300" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
<!-- Tier 2: review -->
<rect x="400" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="480" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
<text x="480" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
<!-- Tier 2: ship -->
<rect x="580" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="660" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
<text x="660" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
<!-- Tier 2: investigate -->
<rect x="760" y="336" width="160" height="48" rx="6" fill="#1c1a17"/>
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="840" y="360" fill="#f1efe7" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
<text x="840" y="372" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
<!-- Legend strip -->
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="428" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(255,106,48,0.10)" stroke="#ff6a30" stroke-width="1"/>
<text x="60" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
<rect x="180" y="444" width="14" height="10" rx="2" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
<text x="200" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(241,239,231,0.05)" stroke="#a8a69d" stroke-width="0.8"/>
<text x="360" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
<text x="500" y="452" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code skill taxonomy · Tree</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Tree · Diagram Design</p>
<h1>Claude Code skill taxonomy</h1>
<p class="subtitle">Three tiers, one root. The full skill library fans out from a single idea — and every leaf descends through exactly one category. Orthogonal connectors, one coral accent, nothing else.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Tier tags (left margin) -->
<text x="40" y="108" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
<text x="40" y="224" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
<text x="40" y="324" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
<!-- Connectors drawn first (behind nodes) -->
<!-- Root → Tier 1 bus -->
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 168 L 500 208" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Design → leaves -->
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Engineering → leaves -->
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Research → single leaf -->
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Root node: Skills (coral focal) -->
<rect x="420" y="80" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="444" y="97" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
<text x="500" y="118" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
<!-- Tier 1: Design -->
<rect x="140" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="140" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="162" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="220" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
<text x="220" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
<!-- Tier 1: Engineering -->
<rect x="420" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="420" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="442" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="500" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
<text x="500" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
<!-- Tier 1: Research -->
<rect x="700" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="700" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="722" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="780" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
<text x="780" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
<!-- Tier 2: polish -->
<rect x="60" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="140" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
<text x="140" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
<!-- Tier 2: critique -->
<rect x="220" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="300" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
<text x="300" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
<!-- Tier 2: review -->
<rect x="400" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="480" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
<text x="480" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
<!-- Tier 2: ship -->
<rect x="580" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="660" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
<text x="660" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
<!-- Tier 2: investigate -->
<rect x="760" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="840" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
<text x="840" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
<!-- Legend strip -->
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="428" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
<rect x="180" y="444" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="360" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
<text x="500" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>One root, one coral dot</h3></div>
<p>A taxonomy is a promise: every child has exactly one parent. Coral lives on the root because that's the only node the tree is actually about — everything else is a descent.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>Connectors are orthogonal</h3></div>
<ul><li>Parent drops vertical</li><li>Horizontal bus connects siblings</li><li>Each child drops into its top edge</li><li>No diagonals, no curves</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>Uneven breadth is honest</h3></div>
<p>Design and Engineering fan to two leaves; Research drops to one. The layout reflects the real shape of the library, not a forced symmetry.</p>
</div>
</div>
<div class="footer">
<span>claude code skill taxonomy</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code skill taxonomy · Tree</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Tree · Diagram Design</p>
<h1>Claude Code skill taxonomy</h1>
<svg viewBox="0 0 1000 480" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Tier tags (left margin) -->
<text x="40" y="108" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">TIER 0 · ROOT</text>
<text x="40" y="224" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 1</text>
<text x="40" y="324" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em" text-anchor="start">TIER 2</text>
<!-- Connectors drawn first (behind nodes) -->
<!-- Root → Tier 1 bus -->
<path d="M 500 128 L 500 168 L 220 168 L 220 208" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 168 L 500 208" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 168 L 780 168 L 780 208" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Design → leaves -->
<path d="M 220 256 L 220 296 L 140 296 L 140 336" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 220 296 L 300 296 L 300 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Engineering → leaves -->
<path d="M 500 256 L 500 296 L 480 296 L 480 336" fill="none" stroke="#52534e" stroke-width="1"/>
<path d="M 500 296 L 660 296 L 660 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Research → single leaf -->
<path d="M 780 256 L 780 296 L 840 296 L 840 336" fill="none" stroke="#52534e" stroke-width="1"/>
<!-- Root node: Skills (coral focal) -->
<rect x="420" y="80" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="420" y="80" width="160" height="48" rx="6" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<rect x="428" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(247,89,31,0.50)" stroke-width="0.8"/>
<text x="444" y="97" fill="#f7591f" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ROOT</text>
<text x="500" y="118" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Skills</text>
<!-- Tier 1: Design -->
<rect x="140" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="140" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="148" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="162" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="220" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Design</text>
<text x="220" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ui · visual · ux</text>
<!-- Tier 1: Engineering -->
<rect x="420" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="420" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="428" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="442" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="500" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Engineering</text>
<text x="500" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">ship · review · test</text>
<!-- Tier 1: Research -->
<rect x="700" y="208" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="700" y="208" width="160" height="48" rx="6" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<rect x="708" y="216" width="28" height="12" rx="2" fill="transparent" stroke="rgba(11,13,11,0.40)" stroke-width="0.8"/>
<text x="722" y="225" fill="#0b0d0b" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CAT</text>
<text x="780" y="240" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Research</text>
<text x="780" y="252" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">investigate · analyze</text>
<!-- Tier 2: polish -->
<rect x="60" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="60" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="140" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">polish</text>
<text x="140" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">align · space · rhythm</text>
<!-- Tier 2: critique -->
<rect x="220" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="220" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="300" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">critique</text>
<text x="300" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">hierarchy · density</text>
<!-- Tier 2: review -->
<rect x="400" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="400" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="480" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">review</text>
<text x="480" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pre-land diff · sql</text>
<!-- Tier 2: ship -->
<rect x="580" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="580" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="660" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ship</text>
<text x="660" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">merge · deploy · verify</text>
<!-- Tier 2: investigate -->
<rect x="760" y="336" width="160" height="48" rx="6" fill="#f5f4ed"/>
<rect x="760" y="336" width="160" height="48" rx="6" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="840" y="360" fill="#0b0d0b" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">investigate</text>
<text x="840" y="372" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">root cause · evidence</text>
<!-- Legend strip -->
<line x1="40" y1="412" x2="960" y2="412" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="428" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<rect x="40" y="444" width="14" height="10" rx="2" fill="rgba(247,89,31,0.08)" stroke="#f7591f" stroke-width="1"/>
<text x="60" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Root · focal</text>
<rect x="180" y="444" width="14" height="10" rx="2" fill="#ffffff" stroke="#0b0d0b" stroke-width="1"/>
<text x="200" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Category branch</text>
<rect x="340" y="444" width="14" height="10" rx="2" fill="rgba(11,13,11,0.05)" stroke="#52534e" stroke-width="0.8"/>
<text x="360" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Leaf skill</text>
<text x="500" y="452" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Orthogonal connectors only. Coral marks the root — every branch descends from one idea.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Good design · Desirable × Feasible × Viable</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #1c1a17;
--color-ink: #f1efe7;
--color-muted: #a8a69d;
--color-accent: #ff6a30;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Venn · Diagram Design</p>
<h1>Good design · Desirable × Feasible × Viable</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(241,239,231,0.10)"/>
</pattern>
<clipPath id="clip-center">
<circle cx="500" cy="180" r="140"/>
</clipPath>
<clipPath id="clip-center-2">
<circle cx="428" cy="320" r="140"/>
</clipPath>
</defs>
<rect width="100%" height="100%" fill="#1c1a17"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
<!-- Desirable — top, ink -->
<circle cx="500" cy="180" r="140" fill="rgba(241,239,231,0.04)" stroke="#f1efe7" stroke-width="1"/>
<!-- Feasible — bottom-left, muted -->
<circle cx="428" cy="320" r="140" fill="rgba(168,166,157,0.05)" stroke="#a8a69d" stroke-width="1"/>
<!-- Viable — bottom-right, soft -->
<circle cx="572" cy="320" r="140" fill="rgba(142,140,131,0.05)" stroke="#8e8c83" stroke-width="1"/>
<!-- Coral focal tint on the all-three intersection -->
<g clip-path="url(#clip-center)">
<g clip-path="url(#clip-center-2)">
<circle cx="572" cy="320" r="140" fill="rgba(255,106,48,0.14)"/>
</g>
</g>
<!-- Set labels -->
<text x="500" y="20" fill="#f1efe7" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
<text x="500" y="36" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
<text x="152" y="400" fill="#a8a69d" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
<text x="152" y="416" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
<text x="848" y="400" fill="#8e8c83" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
<text x="848" y="416" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
<!-- Pairwise intersection labels -->
<text x="360" y="232" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
<text x="360" y="248" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
<text x="640" y="232" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
<text x="640" y="248" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
<text x="500" y="368" fill="#a8a69d" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
<text x="500" y="384" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
<!-- Focal: all-three center -->
<text x="500" y="260" fill="#ff6a30" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
<text x="500" y="276" fill="#a8a69d" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
<!-- Legend strip -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(241,239,231,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#a8a69d" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="none" stroke="#ff6a30" stroke-width="1.2"/>
<text x="68" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
<circle cx="192" cy="488" r="6" fill="none" stroke="#a8a69d" stroke-width="1"/>
<text x="208" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
<text x="380" y="492" fill="#a8a69d" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
</svg>
</div>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Good design · Desirable × Feasible × Viable</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --color-paper:#f5f4ed; --color-paper-2:#efeee5; --color-ink:#0b0d0b; --color-muted:#52534e; --color-soft:#65655c; --color-rule:rgba(11,13,11,0.12); --color-accent:#f7591f; --color-link:#1a70c7; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
.container { max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
svg { width: 100%; min-width: 900px; display: block; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(11,13,11,0.08); }
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
.card h3 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(11,13,11,0.25); font-size: 0.75rem; }
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(11,13,11,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="header-eyebrow">Venn · Diagram Design</p>
<h1>Good design · Desirable × Feasible × Viable</h1>
<p class="subtitle">Three tests every product has to pass before it earns the word "shippable." Miss one and you get a prototype, a vaporware demo, or an internal tool nobody asked for. Coral marks the intersection worth the work.</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<clipPath id="clip-center">
<circle cx="500" cy="180" r="140"/>
</clipPath>
<clipPath id="clip-center-2">
<circle cx="428" cy="320" r="140"/>
</clipPath>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
<!-- Desirable — top, ink -->
<circle cx="500" cy="180" r="140" fill="rgba(11,13,11,0.04)" stroke="#0b0d0b" stroke-width="1"/>
<!-- Feasible — bottom-left, muted -->
<circle cx="428" cy="320" r="140" fill="rgba(82,83,78,0.05)" stroke="#52534e" stroke-width="1"/>
<!-- Viable — bottom-right, soft -->
<circle cx="572" cy="320" r="140" fill="rgba(101,101,92,0.05)" stroke="#65655c" stroke-width="1"/>
<!-- Coral focal tint on the all-three intersection -->
<g clip-path="url(#clip-center)">
<g clip-path="url(#clip-center-2)">
<circle cx="572" cy="320" r="140" fill="rgba(247,89,31,0.10)"/>
</g>
</g>
<!-- Set labels -->
<text x="500" y="20" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
<text x="500" y="36" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
<text x="152" y="400" fill="#52534e" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
<text x="152" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
<text x="848" y="400" fill="#65655c" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
<text x="848" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
<!-- Pairwise intersection labels -->
<text x="360" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
<text x="360" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
<text x="640" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
<text x="640" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
<text x="500" y="368" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
<text x="500" y="384" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
<!-- Focal: all-three center -->
<text x="500" y="260" fill="#f7591f" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
<text x="500" y="276" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
<!-- Legend strip -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="none" stroke="#f7591f" stroke-width="1.2"/>
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
<circle cx="192" cy="488" r="6" fill="none" stroke="#52534e" stroke-width="1"/>
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
<text x="380" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
</svg>
</div>
<div class="cards">
<div class="card">
<p class="eyebrow">THE HEADLINE</p>
<div class="card-header"><span class="card-dot coral"></span><h3>One sweet spot, in coral</h3></div>
<p>Three overlapping circles create seven regions. Six of them are diagnostic — they name the failure mode. Only the center earns the coral. If every region is colored, the diagram stops prioritizing anything.</p>
</div>
<div class="card">
<div class="card-header"><span class="card-dot ink"></span><h3>The three tests</h3></div>
<ul><li>Desirable — someone pulls for it</li><li>Feasible — the team can actually build it</li><li>Viable — the economics hold up over time</li><li>All three, or you don't ship</li></ul>
</div>
<div class="card">
<div class="card-header"><span class="card-dot muted"></span><h3>The named traps</h3></div>
<p>Prototype (loved, buildable, no model). Vaporware (loved, profitable, un-buildable). Internal tool (buildable, profitable, unloved). Labeling each one makes the map more useful than the center alone.</p>
</div>
</div>
<div class="footer">
<span>good design · desirable × feasible × viable</span>
<span>example · diagram-design</span>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Good design · Desirable × Feasible × Viable</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-paper: #f5f4ed;
--color-ink: #0b0d0b;
--color-muted: #52534e;
--color-accent: #f7591f;
--font-sans: 'Geist', system-ui, sans-serif;
--font-serif: 'Instrument Serif', serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
background: var(--color-paper);
color: var(--color-ink);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
}
.frame { max-width: 1200px; width: 100%; }
.eyebrow {
font-family: var(--font-mono);
font-size: 0.66rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-muted);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--color-ink);
margin-bottom: 1.5rem;
}
svg { width: 100%; min-width: 900px; display: block; }
</style>
</head>
<body>
<div class="frame">
<p class="eyebrow">Venn · Diagram Design</p>
<h1>Good design · Desirable × Feasible × Viable</h1>
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(11,13,11,0.10)"/>
</pattern>
<!-- Clip to the all-three intersection (centroid region) for the coral tint -->
<clipPath id="clip-center">
<circle cx="500" cy="180" r="140"/>
</clipPath>
<clipPath id="clip-center-2">
<circle cx="428" cy="320" r="140"/>
</clipPath>
</defs>
<rect width="100%" height="100%" fill="#f5f4ed"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
<!-- Three set circles (stroke + very-low-opacity tint; tints compound in overlaps) -->
<!-- Desirable — top, ink -->
<circle cx="500" cy="180" r="140" fill="rgba(11,13,11,0.04)" stroke="#0b0d0b" stroke-width="1"/>
<!-- Feasible — bottom-left, muted -->
<circle cx="428" cy="320" r="140" fill="rgba(82,83,78,0.05)" stroke="#52534e" stroke-width="1"/>
<!-- Viable — bottom-right, soft -->
<circle cx="572" cy="320" r="140" fill="rgba(101,101,92,0.05)" stroke="#65655c" stroke-width="1"/>
<!-- Coral focal tint on the all-three intersection -->
<g clip-path="url(#clip-center)">
<g clip-path="url(#clip-center-2)">
<circle cx="572" cy="320" r="140" fill="rgba(247,89,31,0.10)"/>
</g>
</g>
<!-- Set labels (outside circles, pinned to far side) -->
<!-- Desirable -->
<text x="500" y="20" fill="#0b0d0b" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Desirable</text>
<text x="500" y="36" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PEOPLE WANT IT</text>
<!-- Feasible -->
<text x="152" y="400" fill="#52534e" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Feasible</text>
<text x="152" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start" letter-spacing="0.14em">WE CAN BUILD IT</text>
<!-- Viable -->
<text x="848" y="400" fill="#65655c" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">Viable</text>
<text x="848" y="416" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">BUSINESS SUSTAINS</text>
<!-- Pairwise intersection labels (inside overlap regions) -->
<!-- Desirable ∩ Feasible (upper-left overlap): "Prototype" — between top and BL -->
<text x="360" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Prototype</text>
<text x="360" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">no business model</text>
<!-- Desirable ∩ Viable (upper-right overlap): "Vaporware" -->
<text x="640" y="232" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vaporware</text>
<text x="640" y="248" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">can't build it</text>
<!-- Feasible ∩ Viable (lower overlap): "Internal tool" -->
<text x="500" y="368" fill="#52534e" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Internal tool</text>
<text x="500" y="384" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">nobody wants it</text>
<!-- Focal: all-three center -->
<text x="500" y="260" fill="#f7591f" font-size="14" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Shippable</text>
<text x="500" y="276" fill="#52534e" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">THE SWEET SPOT</text>
<!-- Legend strip -->
<line x1="40" y1="456" x2="960" y2="456" stroke="rgba(11,13,11,0.10)" stroke-width="0.8"/>
<text x="40" y="472" fill="#52534e" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
<circle cx="52" cy="488" r="6" fill="none" stroke="#f7591f" stroke-width="1.2"/>
<text x="68" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">All three — ship it</text>
<circle cx="192" cy="488" r="6" fill="none" stroke="#52534e" stroke-width="1"/>
<text x="208" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif">Two of three — incomplete</text>
<text x="380" y="492" fill="#52534e" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Coral marks the intersection that earns the work. The others name the traps.</text>
</svg>
</div>
</body>
</html>

9
skills/assets/fonts.css Normal file
View File

@@ -0,0 +1,9 @@
/* html-ppt :: shared webfonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@200;300;400;500;600;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300;400;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,800;1,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');

144
skills/assets/generic.md Normal file
View File

@@ -0,0 +1,144 @@
<!-- Updated: 2026-02-07 -->
# Generic Business SEO Strategy Template
## Overview
This template applies to businesses that don't fit neatly into SaaS, local service, e-commerce, publisher, or agency categories. Customize based on your specific business model.
## Recommended Site Architecture
```
/
├── Home
├── /products (or /services)
│ ├── /product-1
│ ├── /product-2
│ └── ...
├── /solutions (if applicable)
│ ├── /solution-1
│ └── ...
├── /about
│ ├── /team
│ ├── /history
│ └── /values
├── /resources
│ ├── /blog
│ ├── /guides
│ ├── /faq
│ └── /glossary
├── /contact
├── /support
└── /legal
├── /privacy
└── /terms
```
## Universal SEO Principles
### Every Page Should Have
- Unique title tag (30-60 chars)
- Unique meta description (120-160 chars)
- Single H1 matching page intent
- Logical heading hierarchy (H1→H2→H3)
- Internal links to related content
- Clear call-to-action
### Schema for All Sites
| Page Type | Schema Types |
|-----------|-------------|
| Homepage | Organization, WebSite |
| About | Organization, AboutPage |
| Contact | ContactPage |
| Blog | Article, BlogPosting |
| FAQ | (FAQPage only for gov/health) |
| Product/Service | Product or Service |
## Content Quality Standards
### Minimum Word Counts
| Page Type | Min Words |
|-----------|-----------|
| Homepage | 500 |
| Product/Service | 800 |
| Blog Post | 1,500 |
| About Page | 400 |
| Landing Page | 600 |
### E-E-A-T Essentials
1. **Experience**: Share real examples and case studies
2. **Expertise**: Display credentials and qualifications
3. **Authoritativeness**: Earn mentions and citations
4. **Trustworthiness**: Full contact info, policies visible
## Technical Foundations
### Must-Haves
- [ ] HTTPS enabled
- [ ] Mobile-responsive design
- [ ] robots.txt configured
- [ ] XML sitemap submitted
- [ ] Google Search Console verified
- [ ] Core Web Vitals passing (LCP <2.5s, INP <200ms, CLS <0.1)
### Should-Haves
- [ ] Structured data on key pages
- [ ] Internal linking strategy
- [ ] 404 error page optimized
- [ ] Redirect chains eliminated
- [ ] Image optimization (WebP, lazy loading)
## Content Priorities
### Phase 1: Foundation (weeks 1-4)
1. Homepage optimization
2. Core product/service pages
3. About and contact pages
4. Basic schema implementation
### Phase 2: Expansion (weeks 5-12)
1. Blog launch (2-4 posts/month)
2. FAQ page
3. Additional product/service pages
4. Internal linking audit
### Phase 3: Growth (weeks 13-24)
1. Consistent content publishing
2. Link building outreach
3. GEO optimization
4. Performance optimization
### Phase 4: Authority (months 7-12)
1. Thought leadership content
2. Original research
3. PR and media mentions
4. Advanced schema
## Key Metrics to Track
- Organic traffic (overall and by section)
- Keyword rankings (branded and non-branded)
- Conversion rate from organic
- Pages indexed
- Core Web Vitals scores
- Backlinks acquired
## Customization Points
Adjust this template based on:
1. **Business Model**: B2B vs B2C vs D2C
2. **Geographic Scope**: Local, national, or international
3. **Content Type**: Product-focused vs content-heavy
4. **Competition Level**: Niche vs competitive market
5. **Resources**: Budget and team capacity
## Generative Engine Optimization (GEO) Checklist
- [ ] Include clear, quotable facts and statistics that AI systems can extract and cite
- [ ] Use structured data (Schema.org) to help AI systems understand content
- [ ] Build topical authority through comprehensive content clusters
- [ ] Provide original data, research, or unique perspectives AI cannot find elsewhere
- [ ] Maintain consistent entity information (brand, people, products) across the web
- [ ] Structure content with clear headings, definitions, and step-by-step formats
- [ ] Consider adding an `llms.txt` file at site root (emerging convention for AI crawlers: Google treats it as a regular text file)
- [ ] Monitor AI citation across Google AI Overviews, ChatGPT, Perplexity, and Bing Copilot

View File

@@ -0,0 +1,23 @@
Transform the subject into a Funko Pop / Pop Mart blind box style 3D figurine.
Style:
- Cute cartoon proportions (large head, small body)
- 3D rendered (C4D/Octane quality), premium plastic/vinyl finish
- Clean white background, soft studio lighting
Subject handling:
- Person: preserve facial features, hairstyle, clothing
- Animal/Pet: preserve species, fur color, markings
- Object: stylize into cute mascot figurine
- Logo/Icon: transform to 3D toy, preserve original colors and shape
Action: {action}
Caption: "{caption}"
Caption rendering (CRITICAL — follow exactly):
- Black bold text with thick white outline stroke
- Large, clear sans-serif font (e.g. Impact, Helvetica Bold)
- MUST be placed at the absolute bottom center of the image as a standalone text banner
- MUST NOT appear on the character's body, clothing, or any accessory
- Leave visible gap between the character's feet and the caption text
- Text must have sharp anti-aliased edges — it must survive video animation without warping

206
skills/assets/index.html Normal file
View File

@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagram Design · Gallery</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--paper: #f5f4ed;
--paper-2: #efeee5;
--ink: #0b0d0b;
--muted: #52534e;
--soft: #65655c;
--rule: rgba(11,13,11,0.12);
--rule-strong: rgba(11,13,11,0.25);
--accent: #f7591f;
--sans: 'Geist', system-ui, sans-serif;
--serif: 'Instrument Serif', serif;
--mono: 'Geist Mono', ui-monospace, monospace;
}
html, body { height: 100%; }
body {
font-family: var(--sans);
background: var(--paper);
color: var(--ink);
display: flex;
flex-direction: column;
overflow: hidden;
}
.topbar {
padding: 1rem 1.25rem 0.625rem;
border-bottom: 1px solid var(--rule);
flex: 0 0 auto;
}
.title-row {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.75rem;
gap: 1rem;
}
.title-row h1 {
font-family: var(--serif);
font-size: 1.5rem;
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1;
}
.title-row .meta {
font-family: var(--mono);
font-size: 0.66rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.title-row .open-raw {
font-family: var(--mono);
font-size: 0.66rem;
letter-spacing: 0.06em;
color: var(--muted);
text-decoration: none;
padding: 0.3rem 0.625rem;
border: 1px solid var(--rule);
border-radius: 4px;
transition: color 0.15s, border-color 0.15s;
}
.open-raw:hover { color: var(--accent); border-color: var(--accent); }
.tabs {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-bottom: 0.5rem;
}
.tabs.variants {
margin-bottom: 0;
padding-top: 0.5rem;
border-top: 1px solid var(--rule);
}
.tab {
font-family: var(--sans);
font-size: 0.8125rem;
font-weight: 500;
padding: 0.375rem 0.75rem;
background: transparent;
color: var(--muted);
border: 1px solid var(--rule);
border-radius: 4px;
cursor: pointer;
transition: all 0.12s;
letter-spacing: -0.005em;
}
.tab:hover {
color: var(--ink);
border-color: var(--rule-strong);
}
.tab.active {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
.tab .eyebrow {
display: inline-block;
margin-right: 0.25rem;
font-family: var(--mono);
font-size: 0.56rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: inherit;
opacity: 0.5;
}
.tab.new::after {
content: 'NEW';
display: inline-block;
margin-left: 0.35rem;
font-family: var(--mono);
font-size: 0.54rem;
letter-spacing: 0.12em;
color: var(--accent);
vertical-align: 1px;
}
.tab.active.new::after { color: var(--paper); }
main {
flex: 1 1 auto;
position: relative;
background: var(--paper-2);
overflow: hidden;
}
iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
background: var(--paper);
display: block;
}
</style>
</head>
<body>
<header class="topbar">
<div class="title-row">
<h1>Diagram Design <span class="meta" style="font-family: var(--mono); font-size: 0.66rem; margin-left: 0.5rem;">· gallery</span></h1>
<a class="open-raw" id="open-raw" href="#" target="_blank" rel="noopener">open in new tab ↗</a>
</div>
<div class="tabs" id="type-tabs" role="tablist" aria-label="Diagram type">
<button class="tab active" data-type="architecture"><span class="eyebrow">01</span>Architecture</button>
<button class="tab" data-type="flowchart"><span class="eyebrow">02</span>Flowchart</button>
<button class="tab" data-type="sequence"><span class="eyebrow">03</span>Sequence</button>
<button class="tab" data-type="state"><span class="eyebrow">04</span>State</button>
<button class="tab" data-type="er"><span class="eyebrow">05</span>ER</button>
<button class="tab" data-type="timeline"><span class="eyebrow">06</span>Timeline</button>
<button class="tab" data-type="swimlane"><span class="eyebrow">07</span>Swimlane</button>
<button class="tab" data-type="quadrant"><span class="eyebrow">08</span>Quadrant</button>
<button class="tab new" data-type="nested"><span class="eyebrow">09</span>Nested</button>
<button class="tab new" data-type="tree"><span class="eyebrow">10</span>Tree</button>
<button class="tab new" data-type="layers"><span class="eyebrow">11</span>Layers</button>
<button class="tab new" data-type="venn"><span class="eyebrow">12</span>Venn</button>
<button class="tab new" data-type="pyramid"><span class="eyebrow">13</span>Pyramid</button>
</div>
<div class="tabs variants" id="variant-tabs" role="tablist" aria-label="Variant">
<button class="tab active" data-variant="">Minimal light</button>
<button class="tab" data-variant="-dark">Minimal dark</button>
<button class="tab" data-variant="-full">Full editorial</button>
</div>
</header>
<main>
<iframe id="preview" src="example-architecture.html" title="Preview"></iframe>
</main>
<script>
const state = { type: 'architecture', variant: '' };
const iframe = document.getElementById('preview');
const openRaw = document.getElementById('open-raw');
function update() {
const src = `example-${state.type}${state.variant}.html`;
iframe.src = src;
openRaw.href = src;
}
function bindTabs(containerId, key) {
const container = document.getElementById(containerId);
container.addEventListener('click', (e) => {
const btn = e.target.closest('.tab');
if (!btn) return;
container.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
btn.classList.add('active');
state[key] = btn.dataset[key];
update();
});
}
bindTabs('type-tabs', 'type');
bindTabs('variant-tabs', 'variant');
update();
</script>
</body>
</html>

192
skills/assets/ios_frame.jsx Normal file
View File

@@ -0,0 +1,192 @@
/**
* IosFrame — iPhone设备边框
*
* 参考iPhone 15 Pro393×852 logical pixels
* 含:灵动岛 + 状态栏(时间/信号/电池)+ Home Indicator + 圆角
*
* 用法:
* <IosFrame time="9:41" battery={85}>
* <YourAppContent />
* </IosFrame>
*
* 自定义:
* <IosFrame width={390} height={844} darkMode showKeyboard>
* ...
* </IosFrame>
*/
const iosFrameStyles = {
wrapper: {
display: 'inline-block',
padding: 12,
background: '#000',
borderRadius: 60,
boxShadow: '0 0 0 2px #1f2937, 0 20px 60px rgba(0,0,0,0.3)',
position: 'relative',
},
screen: {
position: 'relative',
borderRadius: 48,
overflow: 'hidden',
background: '#fff',
},
statusBar: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 54,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 32px 0 32px',
fontSize: 16,
fontWeight: 600,
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
zIndex: 20,
pointerEvents: 'none',
},
dynamicIsland: {
position: 'absolute',
top: 12,
left: '50%',
transform: 'translateX(-50%)',
width: 124,
height: 36,
background: '#000',
borderRadius: 999,
zIndex: 30,
},
statusIcons: {
display: 'flex',
alignItems: 'center',
gap: 6,
},
signalIcon: {
display: 'flex',
alignItems: 'flex-end',
gap: 2,
height: 12,
},
signalBar: {
width: 3,
background: 'currentColor',
borderRadius: 1,
},
wifiIcon: {
width: 16,
height: 12,
position: 'relative',
},
batteryIcon: {
width: 26,
height: 12,
border: '1.5px solid currentColor',
borderRadius: 3,
padding: 1,
position: 'relative',
opacity: 0.8,
},
batteryCap: {
position: 'absolute',
top: 3,
right: -3,
width: 2,
height: 6,
background: 'currentColor',
borderRadius: '0 1px 1px 0',
},
content: {
position: 'absolute',
top: 54,
left: 0,
right: 0,
bottom: 34,
overflow: 'auto',
},
homeIndicator: {
position: 'absolute',
bottom: 10,
left: '50%',
transform: 'translateX(-50%)',
width: 140,
height: 5,
background: 'rgba(0,0,0,0.3)',
borderRadius: 999,
zIndex: 10,
},
homeIndicatorDark: {
background: 'rgba(255,255,255,0.5)',
},
};
function IosFrame({
children,
width = 393,
height = 852,
time = '9:41',
battery = 100,
darkMode = false,
showStatusBar = true,
showDynamicIsland = true,
showHomeIndicator = true,
}) {
const textColor = darkMode ? '#fff' : '#000';
return (
<div style={iosFrameStyles.wrapper}>
<div style={{
...iosFrameStyles.screen,
width,
height,
background: darkMode ? '#000' : '#fff',
}}>
{showStatusBar && (
<div style={{ ...iosFrameStyles.statusBar, color: textColor }}>
<span>{time}</span>
<div style={iosFrameStyles.statusIcons}>
<div style={iosFrameStyles.signalIcon}>
<div style={{ ...iosFrameStyles.signalBar, height: 4 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 6 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 9 }} />
<div style={{ ...iosFrameStyles.signalBar, height: 11 }} />
</div>
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" style={{ color: textColor }}>
<path d="M8 11.5a1 1 0 100-2 1 1 0 000 2z" fill="currentColor" />
<path d="M3 7.5a7 7 0 0110 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" />
<path d="M1 4.5a11 11 0 0114 0" stroke="currentColor" strokeWidth="1.3" fill="none" strokeLinecap="round" opacity="0.7" />
</svg>
<div style={iosFrameStyles.batteryIcon}>
<div style={{
width: `${battery}%`,
height: '100%',
background: 'currentColor',
borderRadius: 1,
opacity: 0.9,
}} />
<div style={iosFrameStyles.batteryCap} />
</div>
</div>
</div>
)}
{showDynamicIsland && <div style={iosFrameStyles.dynamicIsland} />}
<div style={iosFrameStyles.content}>
{children}
</div>
{showHomeIndicator && (
<div style={{
...iosFrameStyles.homeIndicator,
...(darkMode ? iosFrameStyles.homeIndicatorDark : {}),
}} />
)}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.IosFrame = IosFrame;
}

View File

@@ -0,0 +1,160 @@
<!-- Updated: 2026-02-07 -->
# Local Service Business SEO Strategy Template
## Industry Characteristics
- Geographic-focused searches
- High intent, quick decision making
- Reviews heavily influence decisions
- Phone calls are primary conversion
- Mobile-first user behavior
- Emergency/urgent service needs
## Recommended Site Architecture
```
/
├── Home
├── /services
│ ├── /service-1
│ ├── /service-2
│ └── ...
├── /locations
│ ├── /city-1
│ │ ├── /service-1-city-1
│ │ └── ...
│ ├── /city-2
│ └── ...
├── /about
├── /reviews
├── /gallery (or /portfolio)
├── /blog
├── /contact
├── /emergency (if applicable)
└── /faq
```
## Quality Gates
### Location Page Limits
- ⚠️ **WARNING** at 30+ location pages
- 🛑 **HARD STOP** at 50+ location pages
### Unique Content Requirements
| Page Type | Min Words | Unique % |
|-----------|-----------|----------|
| Primary Location | 600 | 60%+ |
| Service Area | 500 | 40%+ |
| Service Page | 800 | 100% |
### What Makes Location Pages Unique
- Local landmarks and neighborhoods
- Specific services offered at that location
- Local team members
- Location-specific testimonials
- Community involvement
- Local regulations or considerations
## Schema Recommendations
| Page Type | Schema Types |
|-----------|-------------|
| Homepage | LocalBusiness, Organization |
| Service Pages | Service, LocalBusiness |
| Location Pages | LocalBusiness (with geo) |
| Contact | ContactPage, LocalBusiness |
| Reviews | LocalBusiness (with AggregateRating) |
### LocalBusiness Schema Example
```json
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Business Name",
"address": {
"@type": "PostalAddress",
"streetAddress": "123 Main St",
"addressLocality": "City",
"addressRegion": "State",
"postalCode": "12345"
},
"telephone": "+1-555-555-5555",
"openingHours": "Mo-Fr 08:00-18:00",
"geo": {
"@type": "GeoCoordinates",
"latitude": "40.7128",
"longitude": "-74.0060"
},
"areaServed": ["City 1", "City 2"],
"priceRange": "$$"
}
```
## Google Business Profile Integration
- Ensure NAP consistency (Name, Address, Phone)
- Sync service categories
- Regular post updates
- Photo uploads
- Review response strategy
### Google Business Profile Updates (2025-2026)
- **Video verification** is now standard: postcard verification has been largely phased out. Prepare for a short video verification process showing the business location or service area.
- **WhatsApp integration** replaced Google Business Chat (deprecated). Businesses can connect WhatsApp as their primary messaging channel.
- **Q&A removed from Maps**: replaced by AI-generated answers. Ensure your GBP description, services, and website FAQ are comprehensive, as Google AI uses them to answer queries.
- **Business hours are a top-5 ranking factor**: "Business is open at time of search" ranked as a top individual factor for the first time (Whitespark 2026 Local Search Ranking Factors Report). Keep hours accurate; consider extended hours if feasible.
- **Review "Stories" format**: Google Maps now shows review snippets in a swipeable Stories format on mobile. Encourage detailed, descriptive reviews with photos.
### Service Area Business (SAB) Update (June 2025)
Google updated SAB guidelines to **disallow entire states or countries** as service areas. SABs must specify: cities, postal/ZIP codes, or neighborhoods. If you serve an entire metro area, list the major cities within it rather than the state.
### AI Visibility for Local Businesses
AI Overviews appear for only ~0.14% of local keywords (March 2025 data), local SEO faces significantly less AI disruption than other verticals. However, ChatGPT and Perplexity are increasingly used for local recommendations.
To optimize for AI local visibility:
- Ensure presence on expert-curated "best of" lists (ranked #1 AI visibility factor in Whitespark 2026 report)
- Maintain consistent NAP (Name, Address, Phone) across all platforms
- Build genuine review volume and quality
- Use LocalBusiness schema with complete properties (geo, openingHours, priceRange, areaServed)
## Content Priorities
### High Priority
1. Homepage with clear service area
2. Core service pages
3. Primary city page
4. Contact page with all locations
### Medium Priority
1. Service + location combination pages
2. FAQ page
3. About/team page
4. Reviews/testimonials page
### Blog Topics
- Seasonal maintenance tips
- How to choose a [service provider]
- Warning signs of [problem]
- DIY vs professional comparisons
- Local regulations and permits
## Key Metrics to Track
- Local pack rankings
- Phone call volume from organic
- Direction requests
- Google Business Profile insights
- Reviews count and rating
## Generative Engine Optimization (GEO) for Local
- [ ] Include clear, quotable service descriptions and pricing ranges
- [ ] Use LocalBusiness schema with complete geo, openingHours, and areaServed
- [ ] Build presence on curated "best of" and local directory lists
- [ ] Maintain consistent NAP across all platforms (Google, Yelp, Apple Maps)
- [ ] Include original photos of work, team, and location
- [ ] Structure FAQ content for common local service questions
- [ ] Monitor AI citation in ChatGPT and Perplexity local recommendations

View File

@@ -0,0 +1,96 @@
/**
* MacosWindow — macOS应用窗口边框含traffic lights
*
* 用法:
* <MacosWindow title="Finder">
* <YourAppContent />
* </MacosWindow>
*/
const macosWindowStyles = {
window: {
display: 'inline-block',
background: '#fff',
borderRadius: 10,
overflow: 'hidden',
boxShadow: '0 30px 80px rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.15)',
},
titleBar: {
height: 38,
background: 'linear-gradient(to bottom, #e8e8e8, #d8d8d8)',
display: 'flex',
alignItems: 'center',
padding: '0 14px',
borderBottom: '0.5px solid rgba(0,0,0,0.1)',
position: 'relative',
userSelect: 'none',
},
trafficLights: {
display: 'flex',
gap: 8,
alignItems: 'center',
},
light: {
width: 12,
height: 12,
borderRadius: '50%',
border: '0.5px solid rgba(0,0,0,0.15)',
},
close: { background: '#ff5f57' },
minimize: { background: '#febc2e' },
maximize: { background: '#28c840' },
title: {
position: 'absolute',
left: 0,
right: 0,
textAlign: 'center',
fontSize: 13,
color: '#333',
fontWeight: 500,
fontFamily: '-apple-system, "SF Pro Text", sans-serif',
pointerEvents: 'none',
},
content: {
position: 'relative',
overflow: 'auto',
},
titleBarDark: {
background: 'linear-gradient(to bottom, #3c3c3c, #2c2c2c)',
borderBottom: '0.5px solid rgba(255,255,255,0.1)',
},
titleDark: {
color: '#ddd',
},
};
function MacosWindow({ title = '', width = 900, height = 600, darkMode = false, children }) {
return (
<div style={{ ...macosWindowStyles.window, background: darkMode ? '#1e1e1e' : '#fff' }}>
<div style={{
...macosWindowStyles.titleBar,
...(darkMode ? macosWindowStyles.titleBarDark : {}),
}}>
<div style={macosWindowStyles.trafficLights}>
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.close }} />
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.minimize }} />
<div style={{ ...macosWindowStyles.light, ...macosWindowStyles.maximize }} />
</div>
{title && (
<div style={{
...macosWindowStyles.title,
...(darkMode ? macosWindowStyles.titleDark : {}),
}}>
{title}
</div>
)}
</div>
<div style={{ ...macosWindowStyles.content, width, height }}>
{children}
</div>
</div>
);
}
if (typeof window !== 'undefined') {
window.MacosWindow = MacosWindow;
}

View File

@@ -0,0 +1,71 @@
{
"_meta": {
"description": "个人素材索引模板 — 复制此文件并填入你的真实数据",
"how_to_use": "1. 复制此文件到 ~/.claude/memory/personal-asset-index.json 2. 填入你的真实信息 3. design-philosophy skill 会自动读取",
"note": "真实数据文件不要放在 skill 目录内,避免随 skill 分发泄露隐私"
},
"identity": {
"real_name": "你的真名",
"pen_names": ["笔名1", "笔名2"],
"english_name": "English Name",
"title": "你的头衔/一句话介绍",
"bio_short": "50-100字简介",
"bio_long": "200-300字详细介绍",
"avatar_url": "头像URL",
"source": "数据来源备注"
},
"contact": {
"email": "your@email.com",
"wechat_personal": "微信号",
"source": "数据来源备注"
},
"social_media": {
"github": {
"url": "https://github.com/yourname",
"username": "yourname"
},
"youtube": {
"url": "https://www.youtube.com/@YourChannel",
"channel_name": "频道名"
},
"source": "数据来源备注"
},
"websites": {
"main_site": {
"url": "https://yoursite.com",
"description": "网站描述",
"local_path": "/path/to/local/project/"
}
},
"products": {
"product_1": {
"name": "产品名",
"type": "iOS App / Web App / CLI Tool / 电子书",
"achievement": "主要成就",
"icon_path": "/path/to/icon.png",
"project_path": "/path/to/project/"
}
},
"stats": {
"social_followers": "粉丝数",
"product_users": "用户数",
"source": "数据来源备注"
},
"design_assets": {
"article_images": {
"base_path": "/path/to/images/",
"notable_sets": []
}
},
"knowledge_base": {
"wechat_articles": "/path/to/knowledge_base/"
}
}

153
skills/assets/publisher.md Normal file
View File

@@ -0,0 +1,153 @@
<!-- Updated: 2026-02-07 -->
# Publisher/Media SEO Strategy Template
## Industry Characteristics
- High content volume
- Time-sensitive content (news)
- Ad revenue dependent on traffic
- Authority and trust critical
- Competing with social platforms
- AI Overviews impact on traffic
## Recommended Site Architecture
```
/
├── Home
├── /news (or /latest)
├── /topics
│ ├── /topic-1
│ ├── /topic-2
│ └── ...
├── /authors
│ ├── /author-1
│ └── ...
├── /opinion
├── /reviews
├── /guides
├── /videos
├── /podcasts
├── /newsletter
├── /about
│ ├── /editorial-policy
│ ├── /corrections
│ └── /contact
└── /[year]/[month]/[slug] (article URLs)
```
## Schema Recommendations
| Page Type | Schema Types |
|-----------|-------------|
| Article | NewsArticle or Article, Person (author), Organization (publisher) |
| Author Page | Person, ProfilePage |
| Topic Page | CollectionPage, ItemList |
| Homepage | WebSite, Organization |
| Video | VideoObject |
| Podcast | PodcastEpisode, PodcastSeries |
### NewsArticle Schema Example
```json
{
"@context": "https://schema.org",
"@type": "NewsArticle",
"headline": "Article Headline",
"datePublished": "2026-02-07T10:00:00Z",
"dateModified": "2026-02-07T14:30:00Z",
"author": {
"@type": "Person",
"name": "Author Name",
"url": "https://example.com/authors/author-name"
},
"publisher": {
"@type": "Organization",
"name": "Publication Name",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"image": ["https://example.com/article-image.jpg"],
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://example.com/article-url"
}
}
```
## E-E-A-T Requirements
Publishers face highest E-E-A-T scrutiny.
### Author Pages Must Include
- Full name and photo
- Bio and credentials
- Areas of expertise
- Contact information
- Social profiles (sameAs)
- Previous articles by this author
### Editorial Standards
- Clear correction policy
- Transparent editorial process
- Fact-checking procedures
- Conflict of interest disclosures
## Content Priorities
### High Priority
1. Breaking news (speed matters)
2. Evergreen guides on core topics
3. Author pages with credentials
4. Topic hubs/pillar pages
### Medium Priority
1. Opinion/analysis pieces
2. Video content
3. Interactive content
4. Newsletter landing pages
### GEO Considerations
- Clear, quotable facts in articles
- Tables for data-heavy content
- Expert quotes with attribution
- Update dates prominently displayed
- Structured headings (H2/H3)
- First-party data and original research are highly cited by AI systems
- Ensure author entities are clearly defined with Person schema + sameAs links
- Monitor AI citation frequency across Google AI Overviews, AI Mode, ChatGPT, Perplexity
- Treat AI citation as a standalone KPI alongside organic traffic
### Publisher SEO Updates (2025-2026)
- **Google News automatic inclusion:** Google News no longer accepts manual applications (since March 2025). Inclusion is fully automatic based on Google's content quality criteria. Focus on Google News sitemap markup and consistent, high-quality publishing cadence.
- **KPI shift:** Traffic-based KPIs (sessions, pageviews) are declining in relevance as AI Overviews reduce click-through rates. Leading publishers are shifting to: subscriber conversions, time on page, scroll depth, newsletter signups, AI citation frequency, and revenue per visitor.
- **Site reputation abuse risk:** Publishers hosting third-party content (coupons, product reviews, affiliate content) under their domain are at high risk. Google penalized Forbes, WSJ, Time, and CNN for this in late 2024. If hosting third-party content, ensure strong editorial oversight and clear first-party involvement.
## Technical Considerations
### Core Web Vitals
- Ad placement affects CLS
- Lazy load ads and images below fold
- Optimize hero images for LCP
- Minimize render-blocking resources
### AMP (if used)
- Consider dropping AMP (no longer required for Top Stories)
- Ensure canonical setup is correct
- Monitor performance vs non-AMP
### Pagination
- Proper pagination for multi-page articles
- Or infinite scroll with proper indexing
- Canonical to page 1 or full article
## Key Metrics to Track
- Page views from organic
- Time on page
- Pages per session
- Newsletter signups from organic
- Google News/Discover traffic
- AI Overview appearances

879
skills/assets/runtime.js Normal file
View File

@@ -0,0 +1,879 @@
/* html-ppt :: runtime.js
* Keyboard-driven deck runtime. Zero dependencies.
*
* Features:
* ← → / space / PgUp PgDn / Home End navigation
* F fullscreen
* S presenter mode (opens a NEW WINDOW with current/next slide preview + notes + timer)
* The original window stays as audience view, synced via BroadcastChannel.
* Slide previews use CSS transform:scale() at design resolution for pixel-perfect layout.
* N quick notes overlay (bottom drawer)
* O slide overview grid
* T cycle themes (reads data-themes on <html> or <body>)
* A cycle demo animation on current slide
* URL hash #/N deep-link to slide N (1-based)
* Progress bar auto-managed
*/
(function () {
'use strict';
const ANIMS = ['fade-up','fade-down','fade-left','fade-right','rise-in','drop-in',
'zoom-pop','blur-in','glitch-in','typewriter','neon-glow','shimmer-sweep',
'gradient-flow','stagger-list','counter-up','path-draw','parallax-tilt',
'card-flip-3d','cube-rotate-3d','page-turn-3d','perspective-zoom',
'marquee-scroll','kenburns','confetti-burst','spotlight','morph-shape','ripple-reveal'];
function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);}
/* ========== Parse URL for preview-only mode ==========
* When loaded as iframe.src = "index.html?preview=3", runtime enters a
* locked single-slide mode: only slide N is visible, no chrome, no keys,
* no hash updates. This is how the presenter window shows pixel-perfect
* previews — by loading the actual deck file in an iframe and telling it
* to display only a specific slide.
*/
function getPreviewIdx() {
const m = /[?&]preview=(\d+)/.exec(location.search || '');
return m ? parseInt(m[1], 10) - 1 : -1;
}
ready(function () {
const deck = document.querySelector('.deck');
if (!deck) return;
const slides = Array.from(deck.querySelectorAll('.slide'));
if (!slides.length) return;
const previewOnlyIdx = getPreviewIdx();
const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length;
/* ===== Preview-only mode: show one slide, hide everything else ===== */
if (isPreviewMode) {
function showSlide(i) {
slides.forEach((s, j) => {
const active = (j === i);
s.classList.toggle('is-active', active);
s.style.display = active ? '' : 'none';
if (active) {
s.style.opacity = '1';
s.style.transform = 'none';
s.style.pointerEvents = 'auto';
}
});
}
showSlide(previewOnlyIdx);
/* Hide chrome that the presenter shouldn't see in preview */
const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes';
document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; });
document.documentElement.setAttribute('data-preview', '1');
document.body.setAttribute('data-preview', '1');
/* Auto-detect theme base path for theme switching in preview mode */
function getPreviewThemeBase() {
const base = document.documentElement.getAttribute('data-theme-base');
if (base) return base;
const tl = document.getElementById('theme-link');
if (tl) {
const raw = tl.getAttribute('href') || '';
const ls = raw.lastIndexOf('/');
if (ls >= 0) return raw.substring(0, ls + 1);
}
return 'assets/themes/';
}
const previewThemeBase = getPreviewThemeBase();
/* Listen for postMessage from parent presenter window:
* - preview-goto: switch visible slide WITHOUT reloading
* - preview-theme: switch theme CSS link to match audience window */
window.addEventListener('message', function(e) {
if (!e.data) return;
if (e.data.type === 'preview-goto') {
const n = parseInt(e.data.idx, 10);
if (n >= 0 && n < slides.length) showSlide(n);
} else if (e.data.type === 'preview-theme' && e.data.name) {
let link = document.getElementById('theme-link');
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = 'theme-link';
document.head.appendChild(link);
}
link.href = previewThemeBase + e.data.name + '.css';
document.documentElement.setAttribute('data-theme', e.data.name);
}
});
/* Signal to parent that preview iframe is ready */
try { window.parent && window.parent.postMessage({ type: 'preview-ready' }, '*'); } catch(e) {}
return;
}
let idx = 0;
const total = slides.length;
/* ===== BroadcastChannel for presenter sync ===== */
const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname;
let bc;
try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; }
// Are we running inside the presenter popup? (legacy flag, now unused)
const isPresenterWindow = false;
/* ===== progress bar ===== */
let bar = document.querySelector('.progress-bar');
if (!bar) {
bar = document.createElement('div');
bar.className = 'progress-bar';
bar.innerHTML = '<span></span>';
document.body.appendChild(bar);
}
const barFill = bar.querySelector('span');
/* ===== notes overlay (N key) ===== */
let notes = document.querySelector('.notes-overlay');
if (!notes) {
notes = document.createElement('div');
notes.className = 'notes-overlay';
document.body.appendChild(notes);
}
/* ===== overview grid (O key) ===== */
let overview = document.querySelector('.overview');
if (!overview) {
overview = document.createElement('div');
overview.className = 'overview';
slides.forEach((s, i) => {
const t = document.createElement('div');
t.className = 'thumb';
const title = s.getAttribute('data-title') ||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1));
t.innerHTML = '<div class="n">'+(i+1)+'</div><div class="t">'+title.trim().slice(0,80)+'</div>';
t.addEventListener('click', () => { go(i); toggleOverview(false); });
overview.appendChild(t);
});
document.body.appendChild(overview);
}
/* ===== navigation ===== */
function go(n, fromRemote){
n = Math.max(0, Math.min(total-1, n));
slides.forEach((s,i) => {
s.classList.toggle('is-active', i===n);
s.classList.toggle('is-prev', i<n);
});
idx = n;
barFill.style.width = ((n+1)/total*100)+'%';
const numEl = document.querySelector('.slide-number');
if (numEl) { numEl.setAttribute('data-current', n+1); numEl.setAttribute('data-total', total); }
// notes (bottom overlay)
const note = slides[n].querySelector('.notes, aside.notes, .speaker-notes');
notes.innerHTML = note ? note.innerHTML : '';
// hash
const hashTarget = '#/'+(n+1);
if (location.hash !== hashTarget && !isPresenterWindow) {
history.replaceState(null,'', hashTarget);
}
// re-trigger entry animations
slides[n].querySelectorAll('[data-anim]').forEach(el => {
const a = el.getAttribute('data-anim');
el.classList.remove('anim-'+a);
void el.offsetWidth;
el.classList.add('anim-'+a);
});
// counter-up
slides[n].querySelectorAll('.counter').forEach(el => {
const target = parseFloat(el.getAttribute('data-to')||el.textContent);
const dur = parseInt(el.getAttribute('data-dur')||'1200',10);
const start = performance.now();
const from = 0;
function tick(now){
const t = Math.min(1,(now-start)/dur);
const v = from + (target-from)*(1-Math.pow(1-t,3));
el.textContent = (target % 1 === 0) ? Math.round(v) : v.toFixed(1);
if (t<1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
// Broadcast to other window (audience ↔ presenter)
if (!fromRemote && bc) {
bc.postMessage({ type: 'go', idx: n });
}
}
/* ===== listen for remote navigation / theme changes ===== */
if (bc) {
bc.onmessage = function(e) {
if (!e.data) return;
if (e.data.type === 'go' && typeof e.data.idx === 'number') {
go(e.data.idx, true);
} else if (e.data.type === 'theme' && e.data.name) {
/* Sync theme across windows */
const i = themes.indexOf(e.data.name);
if (i >= 0) themeIdx = i;
applyTheme(e.data.name);
}
};
}
function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); }
function toggleOverview(force){ overview.classList.toggle('open', force!==undefined?force:!overview.classList.contains('open')); }
/* ========== PRESENTER MODE — Magnetic-card popup window ========== */
/* Opens a new window with 4 draggable, resizable cards:
* CURRENT — iframe(?preview=N) pixel-perfect preview of current slide
* NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide
* SCRIPT — large speaker notes (逐字稿)
* TIMER — elapsed timer + page counter + controls
* Cards remember position/size in localStorage.
* Two windows sync via BroadcastChannel.
*/
let presenterWin = null;
function openPresenterWindow() {
if (presenterWin && !presenterWin.closed) {
presenterWin.focus();
return;
}
// Build absolute URL of THIS deck file (without hash/query)
const deckUrl = location.protocol + '//' + location.host + location.pathname;
// Collect slide titles + notes (HTML strings)
const slideMeta = slides.map((s, i) => {
const note = s.querySelector('.notes, aside.notes, .speaker-notes');
return {
title: s.getAttribute('data-title') ||
(s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)),
notes: note ? note.innerHTML : ''
};
});
/* Capture current theme so presenter previews match the audience */
const currentTheme = root.getAttribute('data-theme') || (themes[themeIdx] || '');
const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME, currentTheme);
presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no');
if (!presenterWin) {
alert('请允许弹出窗口以使用演讲者视图');
return;
}
presenterWin.document.open();
presenterWin.document.write(presenterHTML);
presenterWin.document.close();
}
function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName, currentTheme) {
const metaJSON = JSON.stringify(slideMeta);
const deckUrlJSON = JSON.stringify(deckUrl);
const channelJSON = JSON.stringify(channelName);
const themeJSON = JSON.stringify(currentTheme || '');
const storageKey = 'html-ppt-presenter:' + location.pathname;
// Build the document as a single template string for clarity
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Presenter View</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%; overflow: hidden;
background: #1a1d24;
background-image:
radial-gradient(circle at 20% 30%, rgba(88,166,255,.04), transparent 50%),
radial-gradient(circle at 80% 70%, rgba(188,140,255,.04), transparent 50%);
color: #e6edf3;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
}
/* Stage: positioned area where cards live */
#stage { position: absolute; inset: 0; overflow: hidden; }
/* Magnetic card */
.pcard {
position: absolute;
background: #0d1117;
border: 1px solid rgba(255,255,255,.1);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,.45), 0 0 0 1px rgba(255,255,255,.02);
display: flex; flex-direction: column;
overflow: hidden;
min-width: 180px; min-height: 100px;
transition: box-shadow .2s, border-color .2s;
}
.pcard.dragging { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(88,166,255,.5); border-color: #58a6ff; transition: none; z-index: 9999; }
.pcard.resizing { box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 2px rgba(63,185,80,.5); border-color: #3fb950; transition: none; z-index: 9999; }
.pcard:hover { border-color: rgba(88,166,255,.3); }
/* Card header (drag handle) */
.pcard-head {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: rgba(255,255,255,.04);
border-bottom: 1px solid rgba(255,255,255,.06);
cursor: move;
user-select: none;
flex-shrink: 0;
}
.pcard-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--dot-color, #58a6ff); flex-shrink: 0; }
.pcard-title {
font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
font-weight: 700; color: #8b949e; flex: 1;
}
.pcard-meta { font-size: 11px; color: #6e7681; }
/* Card body */
.pcard-body { flex: 1; position: relative; overflow: hidden; min-height: 0; }
/* Preview cards (CURRENT/NEXT) — iframe-based pixel-perfect render */
.pcard-preview .pcard-body { background: #000; }
.pcard-preview iframe {
position: absolute; top: 0; left: 0;
width: 1920px; height: 1080px;
border: none;
transform-origin: top left;
pointer-events: none;
background: transparent;
}
.pcard-preview .preview-end {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: #484f58; font-size: 14px; letter-spacing: .12em;
}
/* Notes card */
.pcard-notes .pcard-body {
padding: 14px 18px;
overflow-y: auto;
font-size: 18px; line-height: 1.75;
color: #d0d7de;
font-family: "Noto Sans SC", -apple-system, sans-serif;
}
.pcard-notes .pcard-body p { margin: 0 0 .7em 0; }
.pcard-notes .pcard-body strong { color: #f0883e; }
.pcard-notes .pcard-body em { color: #58a6ff; font-style: normal; }
.pcard-notes .pcard-body code {
font-family: "SF Mono", monospace; font-size: .9em;
background: rgba(255,255,255,.08); padding: 1px 6px; border-radius: 4px;
}
.pcard-notes .empty { color: #484f58; font-style: italic; }
/* Timer card */
.pcard-timer .pcard-body {
display: flex; flex-direction: column; gap: 14px;
padding: 18px 20px; justify-content: center;
}
.timer-display {
font-family: "SF Mono", "JetBrains Mono", monospace;
font-size: 42px; font-weight: 700;
color: #3fb950;
letter-spacing: .04em;
line-height: 1;
}
.timer-row {
display: flex; align-items: center; gap: 12px;
font-size: 14px; color: #8b949e;
}
.timer-row .label { font-size: 10px; letter-spacing: .15em; text-transform: uppercase; color: #6e7681; }
.timer-row .val { color: #e6edf3; font-weight: 600; font-family: "SF Mono", monospace; }
.timer-controls { display: flex; gap: 8px; flex-wrap: wrap; }
.timer-btn {
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.1);
color: #e6edf3;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
}
.timer-btn:hover { background: rgba(88,166,255,.15); border-color: #58a6ff; }
.timer-btn:active { transform: translateY(1px); }
/* Resize handle */
.pcard-resize {
position: absolute; right: 0; bottom: 0;
width: 18px; height: 18px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,.25) 50%, rgba(255,255,255,.25) 60%, transparent 60%, transparent 70%, rgba(255,255,255,.25) 70%, rgba(255,255,255,.25) 80%, transparent 80%);
z-index: 5;
}
.pcard-resize:hover { background: linear-gradient(135deg, transparent 50%, #58a6ff 50%, #58a6ff 60%, transparent 60%, transparent 70%, #58a6ff 70%, #58a6ff 80%, transparent 80%); }
/* Bottom hint bar */
.hint-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: rgba(0,0,0,.6);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255,255,255,.08);
padding: 6px 16px;
font-size: 11px; color: #8b949e;
display: flex; gap: 18px; align-items: center;
z-index: 1000;
}
.hint-bar kbd {
background: rgba(255,255,255,.08);
padding: 1px 6px; border-radius: 3px;
font-family: "SF Mono", monospace;
font-size: 10px;
border: 1px solid rgba(255,255,255,.1);
color: #e6edf3;
}
.hint-bar .reset-layout {
margin-left: auto;
background: transparent; border: 1px solid rgba(255,255,255,.15);
color: #8b949e; padding: 3px 10px; border-radius: 4px;
font-size: 11px; cursor: pointer; font-family: inherit;
}
.hint-bar .reset-layout:hover { background: rgba(248,81,73,.15); border-color: #f85149; color: #f85149; }
body.is-dragging-card * { user-select: none !important; }
body.is-dragging-card iframe { pointer-events: none !important; }
</style>
</head>
<body>
<div id="stage">
<div class="pcard pcard-preview" id="card-cur" style="--dot-color:#58a6ff">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">CURRENT</span>
<span class="pcard-meta" id="cur-meta">—</span>
</div>
<div class="pcard-body"><iframe id="iframe-cur"></iframe></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-preview" id="card-nxt" style="--dot-color:#bc8cff">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">NEXT</span>
<span class="pcard-meta" id="nxt-meta">—</span>
</div>
<div class="pcard-body"><iframe id="iframe-nxt"></iframe></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-notes" id="card-notes" style="--dot-color:#f0883e">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">SPEAKER SCRIPT · 逐字稿</span>
</div>
<div class="pcard-body" id="notes-body"></div>
<div class="pcard-resize" data-resize></div>
</div>
<div class="pcard pcard-timer" id="card-timer" style="--dot-color:#3fb950">
<div class="pcard-head" data-drag>
<span class="pcard-dot"></span>
<span class="pcard-title">TIMER</span>
</div>
<div class="pcard-body">
<div class="timer-display" id="timer-display">00:00</div>
<div class="timer-row">
<span class="label">Slide</span>
<span class="val" id="timer-count">1 / ${total}</span>
</div>
<div class="timer-controls">
<button class="timer-btn" id="btn-prev">← Prev</button>
<button class="timer-btn" id="btn-next">Next →</button>
<button class="timer-btn" id="btn-reset">⏱ Reset</button>
</div>
</div>
<div class="pcard-resize" data-resize></div>
</div>
</div>
<div class="hint-bar">
<span><kbd>← →</kbd> 翻页</span>
<span><kbd>R</kbd> 重置计时</span>
<span><kbd>Esc</kbd> 关闭</span>
<span style="color:#6e7681">拖动卡片头部移动 · 拖动右下角调整大小</span>
<button class="reset-layout" id="reset-layout">重置布局</button>
</div>
<script>
(function(){
var slideMeta = ${metaJSON};
var total = ${total};
var idx = ${startIdx};
var deckUrl = ${deckUrlJSON};
var STORAGE_KEY = ${JSON.stringify(storageKey)};
var bc;
try { bc = new BroadcastChannel(${channelJSON}); } catch(e) {}
var iframeCur = document.getElementById('iframe-cur');
var iframeNxt = document.getElementById('iframe-nxt');
var notesBody = document.getElementById('notes-body');
var curMeta = document.getElementById('cur-meta');
var nxtMeta = document.getElementById('nxt-meta');
var timerDisplay = document.getElementById('timer-display');
var timerCount = document.getElementById('timer-count');
/* ===== Default card layout ===== */
function defaultLayout() {
var w = window.innerWidth;
var h = window.innerHeight - 36; /* leave room for hint bar */
return {
'card-cur': { x: 16, y: 16, w: Math.round(w*0.55) - 24, h: Math.round(h*0.62) - 16 },
'card-nxt': { x: Math.round(w*0.55) + 8, y: 16, w: w - Math.round(w*0.55) - 24, h: Math.round(h*0.42) - 16 },
'card-notes': { x: Math.round(w*0.55) + 8, y: Math.round(h*0.42) + 8, w: w - Math.round(w*0.55) - 24, h: h - Math.round(h*0.42) - 16 },
'card-timer': { x: 16, y: Math.round(h*0.62) + 8, w: Math.round(w*0.55) - 24, h: h - Math.round(h*0.62) - 16 }
};
}
/* ===== Apply / save / restore layout ===== */
function applyLayout(layout) {
Object.keys(layout).forEach(function(id){
var el = document.getElementById(id);
var l = layout[id];
if (el && l) {
el.style.left = l.x + 'px';
el.style.top = l.y + 'px';
el.style.width = l.w + 'px';
el.style.height = l.h + 'px';
}
});
rescaleAll();
}
function readLayout() {
try {
var saved = localStorage.getItem(STORAGE_KEY);
if (saved) return JSON.parse(saved);
} catch(e) {}
return defaultLayout();
}
function saveLayout() {
var layout = {};
['card-cur','card-nxt','card-notes','card-timer'].forEach(function(id){
var el = document.getElementById(id);
if (el) {
layout[id] = {
x: parseInt(el.style.left,10) || 0,
y: parseInt(el.style.top,10) || 0,
w: parseInt(el.style.width,10) || 300,
h: parseInt(el.style.height,10) || 200
};
}
});
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); } catch(e) {}
}
/* ===== iframe rescale to fit card body ===== */
function rescaleIframe(iframe) {
if (!iframe || iframe.style.display === 'none') return;
var body = iframe.parentElement;
var cw = body.clientWidth, ch = body.clientHeight;
if (!cw || !ch) return;
var s = Math.min(cw / 1920, ch / 1080);
iframe.style.transform = 'scale(' + s + ')';
/* Center the scaled iframe in the body */
var sw = 1920 * s, sh = 1080 * s;
iframe.style.left = Math.max(0, (cw - sw) / 2) + 'px';
iframe.style.top = Math.max(0, (ch - sh) / 2) + 'px';
}
function rescaleAll() {
rescaleIframe(iframeCur);
rescaleIframe(iframeNxt);
}
window.addEventListener('resize', rescaleAll);
/* ===== Drag (move card by header) ===== */
document.querySelectorAll('[data-drag]').forEach(function(handle){
handle.addEventListener('mousedown', function(e){
if (e.button !== 0) return;
var card = handle.closest('.pcard');
if (!card) return;
e.preventDefault();
card.classList.add('dragging');
document.body.classList.add('is-dragging-card');
var startX = e.clientX, startY = e.clientY;
var startL = parseInt(card.style.left,10) || 0;
var startT = parseInt(card.style.top,10) || 0;
function onMove(ev){
var nx = Math.max(0, Math.min(window.innerWidth - 100, startL + ev.clientX - startX));
var ny = Math.max(0, Math.min(window.innerHeight - 50, startT + ev.clientY - startY));
card.style.left = nx + 'px';
card.style.top = ny + 'px';
}
function onUp(){
card.classList.remove('dragging');
document.body.classList.remove('is-dragging-card');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
saveLayout();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
/* ===== Resize (drag bottom-right corner) ===== */
document.querySelectorAll('[data-resize]').forEach(function(handle){
handle.addEventListener('mousedown', function(e){
if (e.button !== 0) return;
var card = handle.closest('.pcard');
if (!card) return;
e.preventDefault(); e.stopPropagation();
card.classList.add('resizing');
document.body.classList.add('is-dragging-card');
var startX = e.clientX, startY = e.clientY;
var startW = parseInt(card.style.width,10) || card.offsetWidth;
var startH = parseInt(card.style.height,10) || card.offsetHeight;
function onMove(ev){
var nw = Math.max(180, startW + ev.clientX - startX);
var nh = Math.max(100, startH + ev.clientY - startY);
card.style.width = nw + 'px';
card.style.height = nh + 'px';
if (card.querySelector('iframe')) rescaleIframe(card.querySelector('iframe'));
}
function onUp(){
card.classList.remove('resizing');
document.body.classList.remove('is-dragging-card');
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
rescaleAll();
saveLayout();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
/* ===== Preview iframe ready tracking =====
* Each iframe loads the deck ONCE with ?preview=1 on init. Subsequent
* slide changes are sent via postMessage('preview-goto') so the iframe
* just toggles visibility of a different .slide — no reload, no flicker.
*/
var iframeReady = { cur: false, nxt: false };
var currentTheme = ${themeJSON};
window.addEventListener('message', function(e) {
if (!e.data || e.data.type !== 'preview-ready') return;
var iframe = null;
if (e.source === iframeCur.contentWindow) {
iframeReady.cur = true;
iframe = iframeCur;
postPreviewGoto(iframeCur, idx);
} else if (e.source === iframeNxt.contentWindow) {
iframeReady.nxt = true;
iframe = iframeNxt;
postPreviewGoto(iframeNxt, idx + 1 < total ? idx + 1 : idx);
}
/* Sync current theme to the iframe */
if (iframe && currentTheme) {
try { iframe.contentWindow.postMessage({ type: 'preview-theme', name: currentTheme }, '*'); } catch(err) {}
}
if (iframe) rescaleIframe(iframe);
});
function postPreviewGoto(iframe, n) {
try {
iframe.contentWindow.postMessage({ type: 'preview-goto', idx: n }, '*');
} catch(e) {}
}
/* ===== Update content =====
* Smooth (no-reload) navigation: send postMessage to iframes instead of
* resetting src. Iframes stay loaded, just switch visible .slide.
*/
function update(n) {
n = Math.max(0, Math.min(total - 1, n));
idx = n;
/* Current preview — postMessage (smooth) */
if (iframeReady.cur) postPreviewGoto(iframeCur, n);
curMeta.textContent = (n + 1) + '/' + total;
/* Next preview */
if (n + 1 < total) {
iframeNxt.style.display = '';
var endEl = document.querySelector('#card-nxt .preview-end');
if (endEl) endEl.remove();
if (iframeReady.nxt) postPreviewGoto(iframeNxt, n + 1);
nxtMeta.textContent = (n + 2) + '/' + total;
} else {
iframeNxt.style.display = 'none';
var body = document.querySelector('#card-nxt .pcard-body');
if (body && !body.querySelector('.preview-end')) {
var end = document.createElement('div');
end.className = 'preview-end';
end.textContent = '— END OF DECK —';
body.appendChild(end);
}
nxtMeta.textContent = 'END';
}
/* Notes */
var note = slideMeta[n].notes;
notesBody.innerHTML = note || '<span class="empty">(这一页还没有逐字稿)</span>';
/* Timer count */
timerCount.textContent = (n + 1) + ' / ' + total;
}
/* ===== Timer ===== */
var tStart = Date.now();
setInterval(function(){
var s = Math.floor((Date.now() - tStart) / 1000);
var mm = String(Math.floor(s/60)).padStart(2,'0');
var ss = String(s%60).padStart(2,'0');
timerDisplay.textContent = mm + ':' + ss;
}, 1000);
function resetTimer(){ tStart = Date.now(); timerDisplay.textContent = '00:00'; }
/* ===== BroadcastChannel sync ===== */
if (bc) {
bc.onmessage = function(e){
if (!e.data) return;
if (e.data.type === 'go') update(e.data.idx);
else if (e.data.type === 'theme' && e.data.name) {
currentTheme = e.data.name;
/* Forward theme change to preview iframes */
[iframeCur, iframeNxt].forEach(function(iframe){
try {
iframe.contentWindow.postMessage({ type: 'preview-theme', name: e.data.name }, '*');
} catch(err) {}
});
}
};
}
function go(n) {
update(n);
if (bc) bc.postMessage({ type: 'go', idx: idx });
}
/* ===== Buttons ===== */
document.getElementById('btn-prev').addEventListener('click', function(){ go(idx - 1); });
document.getElementById('btn-next').addEventListener('click', function(){ go(idx + 1); });
document.getElementById('btn-reset').addEventListener('click', resetTimer);
document.getElementById('reset-layout').addEventListener('click', function(){
if (confirm('恢复默认卡片布局?')) {
try { localStorage.removeItem(STORAGE_KEY); } catch(e){}
applyLayout(defaultLayout());
}
});
/* ===== Keyboard ===== */
document.addEventListener('keydown', function(e){
if (e.metaKey || e.ctrlKey || e.altKey) return;
switch(e.key) {
case 'ArrowRight': case ' ': case 'PageDown': go(idx + 1); e.preventDefault(); break;
case 'ArrowLeft': case 'PageUp': go(idx - 1); e.preventDefault(); break;
case 'Home': go(0); break;
case 'End': go(total - 1); break;
case 'r': case 'R': resetTimer(); break;
case 'Escape': window.close(); break;
}
});
/* ===== Iframe load → rescale (catches initial size) ===== */
iframeCur.addEventListener('load', function(){ rescaleIframe(iframeCur); });
iframeNxt.addEventListener('load', function(){ rescaleIframe(iframeNxt); });
/* ===== Init =====
* Load each iframe ONCE with the deck file. After they post
* 'preview-ready', all subsequent navigation is via postMessage
* (smooth, no reload, no flicker).
*/
applyLayout(readLayout());
iframeCur.src = deckUrl + '?preview=' + (idx + 1);
if (idx + 1 < total) iframeNxt.src = deckUrl + '?preview=' + (idx + 2);
/* Initialize notes/timer/count without touching iframes */
notesBody.innerHTML = slideMeta[idx].notes || '<span class="empty">(这一页还没有逐字稿)</span>';
curMeta.textContent = (idx + 1) + '/' + total;
nxtMeta.textContent = (idx + 2) + '/' + total;
timerCount.textContent = (idx + 1) + ' / ' + total;
})();
</` + `script>
</body></html>`;
}
function fullscreen(){ const el=document.documentElement;
if (!document.fullscreenElement) el.requestFullscreen&&el.requestFullscreen();
else document.exitFullscreen&&document.exitFullscreen();
}
// theme cycling
const root = document.documentElement;
const themesAttr = root.getAttribute('data-themes') || document.body.getAttribute('data-themes');
const themes = themesAttr ? themesAttr.split(',').map(s=>s.trim()).filter(Boolean) : [];
let themeIdx = 0;
// Auto-detect theme base path from existing <link id="theme-link">
let themeBase = root.getAttribute('data-theme-base');
if (!themeBase) {
const existingLink = document.getElementById('theme-link');
if (existingLink) {
// el.getAttribute('href') gives the raw relative path written in HTML
const rawHref = existingLink.getAttribute('href') || '';
const lastSlash = rawHref.lastIndexOf('/');
themeBase = lastSlash >= 0 ? rawHref.substring(0, lastSlash + 1) : 'assets/themes/';
} else {
themeBase = 'assets/themes/';
}
}
function applyTheme(name) {
let link = document.getElementById('theme-link');
if (!link) {
link = document.createElement('link');
link.rel = 'stylesheet';
link.id = 'theme-link';
document.head.appendChild(link);
}
link.href = themeBase + name + '.css';
root.setAttribute('data-theme', name);
const ind = document.querySelector('.theme-indicator');
if (ind) ind.textContent = name;
}
function cycleTheme(fromRemote){
if (!themes.length) return;
themeIdx = (themeIdx+1) % themes.length;
const name = themes[themeIdx];
applyTheme(name);
/* Broadcast to other window (audience ↔ presenter) */
if (!fromRemote && bc) bc.postMessage({ type: 'theme', name: name });
}
// animation cycling on current slide
let animIdx = 0;
function cycleAnim(){
animIdx = (animIdx+1) % ANIMS.length;
const a = ANIMS[animIdx];
const target = slides[idx].querySelector('[data-anim-target]') || slides[idx];
ANIMS.forEach(x => target.classList.remove('anim-'+x));
void target.offsetWidth;
target.classList.add('anim-'+a);
target.setAttribute('data-anim', a);
const ind = document.querySelector('.anim-indicator');
if (ind) ind.textContent = a;
}
document.addEventListener('keydown', function (e) {
if (e.metaKey||e.ctrlKey||e.altKey) return;
switch (e.key) {
case 'ArrowRight': case ' ': case 'PageDown': case 'Enter': go(idx+1); e.preventDefault(); break;
case 'ArrowLeft': case 'PageUp': case 'Backspace': go(idx-1); e.preventDefault(); break;
case 'Home': go(0); break;
case 'End': go(total-1); break;
case 'f': case 'F': fullscreen(); break;
case 's': case 'S': openPresenterWindow(); break;
case 'n': case 'N': toggleNotes(); break;
case 'o': case 'O': toggleOverview(); break;
case 't': case 'T': cycleTheme(); break;
case 'a': case 'A': cycleAnim(); break;
case 'Escape': toggleOverview(false); toggleNotes(false); break;
}
});
// hash deep-link
function fromHash(){
const m = /^#\/(\d+)/.exec(location.hash||'');
if (m) go(Math.max(0, parseInt(m[1],10)-1));
}
window.addEventListener('hashchange', fromHash);
fromHash();
go(idx);
});
})();

135
skills/assets/saas.md Normal file
View File

@@ -0,0 +1,135 @@
<!-- Updated: 2026-02-07 -->
# SaaS SEO Strategy Template
## Industry Characteristics
- Long sales cycles with multiple touchpoints
- Feature-focused decision making
- Comparison shopping behavior
- Heavy research phase before purchase
- Integration and ecosystem considerations
## Recommended Site Architecture
```
/
├── Home
├── /product (or /platform)
│ ├── /features
│ │ ├── /feature-1
│ │ ├── /feature-2
│ │ └── ...
│ ├── /integrations
│ │ ├── /integration-1
│ │ └── ...
│ └── /security
├── /solutions
│ ├── /by-industry
│ │ ├── /industry-1
│ │ └── ...
│ └── /by-use-case
│ ├── /use-case-1
│ └── ...
├── /pricing
├── /customers
│ ├── /case-studies
│ │ ├── /case-study-1
│ │ └── ...
│ └── /testimonials
├── /resources
│ ├── /blog
│ ├── /guides
│ ├── /webinars
│ ├── /templates
│ └── /glossary
├── /docs (or /help)
│ └── /api
├── /company
│ ├── /about
│ ├── /careers
│ ├── /press
│ └── /contact
└── /compare
├── /vs-competitor-1
└── /vs-competitor-2
```
## Content Priorities
### High Priority Pages
1. Homepage (value proposition, social proof)
2. Features overview
3. Pricing page
4. Key integrations
5. Top 3-5 use case pages
### Medium Priority Pages
1. Individual feature pages
2. Industry solution pages
3. Case studies (2-3 detailed ones)
4. Comparison pages (vs competitors)
### Content Marketing Focus
1. Bottom-of-funnel: Comparison guides, ROI calculators
2. Middle-of-funnel: How-to guides, best practices
3. Top-of-funnel: Industry trends, educational content
## Schema Recommendations
| Page Type | Schema Types |
|-----------|-------------|
| Homepage | Organization, WebSite, SoftwareApplication |
| Product/Features | SoftwareApplication, Offer |
| Pricing | SoftwareApplication, Offer (with pricing) |
| Blog | Article, BlogPosting |
| Case Studies | Article, Organization (customer) |
| Documentation | TechArticle |
## Key Metrics to Track
- Organic traffic to pricing page
- Demo/trial signups from organic
- Blog → pricing page conversion
- Comparison page rankings
- Integration page performance
## Comparison & Alternative Pages
Comparison pages are among the highest-converting content types for SaaS, with conversion rates of **4-7%** vs. 0.5-1.8% for standard blog content (35.8% of marketers report comparison content performs "better than ever" per Intergrowth November 2025 survey).
**Recommended page types:**
- `/{product}-vs-{competitor}`: Direct 1:1 comparison
- `/{competitor}-alternative`: Targeting competitor brand searches
- `/compare/{category}`: Category comparison hub
- `/best-{category}-tools`: Roundup-style pages
**Best practices:**
- Include structured comparison tables with pricing, features, pros/cons
- Be factually accurate about competitors: verify claims regularly
- Include customer testimonials from users who switched
- Add FAQ schema for common comparison questions (valuable for AI search)
- Update regularly: stale comparison data damages credibility
- Cross-reference the `seo-competitor-pages` skill for detailed frameworks
**Legal considerations:**
- Nominative fair use generally permits competitor brand mentions for comparison purposes
- Do NOT imply endorsement or affiliation
- Do NOT make false or unverifiable claims about competitor products
- Different jurisdictions have different trademark laws: consult legal counsel
## Competitive Considerations
- Monitor competitor feature releases
- Track competitor content strategies
- Identify keyword gaps in feature coverage
- Watch for new comparison opportunities
## Generative Engine Optimization (GEO) for SaaS
- [ ] Include clear, structured feature comparisons that AI systems can parse and cite
- [ ] Use SoftwareApplication schema with complete feature lists and pricing
- [ ] Publish original benchmark data, case studies, and ROI metrics
- [ ] Build content clusters around key product categories and use cases
- [ ] Ensure integration pages have clear, quotable descriptions
- [ ] Structure pricing information in tables AI can extract
- [ ] Monitor AI citation across Google AI Overviews, ChatGPT, and Perplexity

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More