Phase 2 of 10

🎨 Design System

Visual language for consistent UI

10 sub-phases
60 pros
50 cons
83 gotchas
50 anti-patterns
8613 chars code
2.1

🎨 Color Tokens (Semantic Palette)

Intermediate⏱ 1 hour

Map visual colors (gray-500, blue-600) to semantic roles (text-primary, surface-raised) so theme switching and white-labeling become data-attribute changes.

🎯 When to use: Multi-theme product; white-label SaaS; design system spanning > 1 app; you need dark mode; brand refresh every 6 months
⛔ When NOT to use
• Single-page landing with 5 elements
• one-off marketing site
• prototype that ships once and dies
• no theming requirement
✅ Pros (6)
• Theme switch = 1-line CSS change (`data-theme=dark`) — zero JS, zero rebuild;
• Word 'primary' survives a brand refresh where 'blue' would force a 200-file rename;
• WCAG contrast checks happen against semantic roles, not raw values — 1 audit covers every theme;
• White-label fork from 100 → 1 codebase: client picks theme JSON at deploy time;
• Designers and devs speak the same vocabulary in Figma Variables + code (token name parity);
• Accessibility audits become mechanical: scan token→pair pairs, not 5000 hex literals.
⚠️ Cons (5)
• Indirection tax — juniors grep 'primary' and miss the actual value;
• Token overlap ('accent' vs 'brand') confuses teams if not enforced in lint;
• HSL/oklch math is required to derive shades from one base — wrong math = ugly ramps;
• Tailwind already does this — adopting tokens on top adds a layer you must maintain;
• Print/email fallbacks still need raw hex — tokens leak into non-browser contexts.
⚡ Gotchas (8)
• Pure #000 on pure #fff hits WCAG AAA but feels harsh — use #0a0a14 / #f5f5f7 for softer contrast that still passes AA (4.5:1);
• HSL hue rotation for variant shades shifts color identity — use oklch or HSL with locked hue; rotate lightness only for a true ramp;
• Form controls (input, select, checkbox) ignore CSS background by default — explicit `accent-color` and `color-scheme: dark` are required;
• iOS Safari auto-applies blue/yellow tint to inputs in dark mode unless `color-scheme: dark` is set on `:root`;
• Images baked in light mode look like flashlights in dark — apply `filter: invert(1) hue-rotate(180deg)` to <img> or use SVG;
• Charts (Chart.js, D3) hardcode colors — read tokens via getComputedStyle, don't import CSS into JS;
• Transparent tokens (`--surface: hsl(... / 50%)`) need a separate `--surface-solid` for accessibility overlays or backgrounds stack visibly;
• Shadow tokens don't theme — shadows look wrong on dark bg unless `--shadow-color` is also a token (alpha-included).
🚫 Anti-patterns
• Naming tokens by color: `--blue-500` instead of `--color-primary-500` — they become useless the moment brand changes;
• Defining 50 shades when the ramp needs 5 (50/100/200/500/900) — every shade is a future decision you forgot;
• Storing tokens in JS only — sync drift between Figma and code; use CSS custom props or Style Dictionary;
• Using hex when HSL/oklch would let you derive variants with one knob (lightness);
• Skipping the 'danger/warning/info/success' family because 'the app never shows errors' — it will, in week 3.
🔀 Alternatives
vs 1.2 (raw CSS variables without semantic naming)
vs Tailwind config (built-in but JS-bound)
vs Style Dictionary (multi-platform export)
📚 Prereqs: 1.2
➡️ Next: 2.2, 2.5, 2.8
💻 Minimal (106 chars) ▶ Full example (673 chars)
:root { --color-bg:hsl(240 10% 5%); --color-text-primary:hsl(0 0% 96%); --color-accent:hsl(330 81% 60%); }
:root {\n  --color-bg: hsl(240 10% 5%);\n  --color-surface: hsl(240 10% 9%);\n  --color-text-primary: hsl(0 0% 96%);\n  --color-text-muted: hsl(240 5% 65%);\n  --color-accent: hsl(330 81% 60%);\n  --color-success: hsl(142 71% 45%);\n  --color-warning: hsl(38 92% 50%);\n  --color-danger: hsl(0 84% 60%);\n  --color-border: hsl(240 6% 20%);\n}\n[data-theme=\"light\"] {\n  --color-bg: hsl(0 0% 98%);\n  --color-surface: hsl(0 0% 100%);\n  --color-text-primary: hsl(240 10% 10%);\n  --color-text-muted: hsl(240 5% 40%);\n}\n:root { color-scheme: dark light; }\n.btn { background: var(--color-accent); color: var(--color-text-primary); border: 1px solid var(--color-border); }
colortokenswcagdesign-systemthemeสีธีม
2.2

🔤 Typography Scale (Modular)

Beginner⏱ 30 min

Pick a ratio (1.250 / 1.333 / 1.414 / 1.5) and generate a type ramp xs→5xl with consistent visual rhythm — stops decision fatigue and survives redesigns.

🎯 When to use: UI with > 3 distinct text sizes; design system spanning multiple pages; need consistent hierarchy in marketing + product UI; brand guidelines exist
⛔ When NOT to use
• Single landing with hero + body + footer
• print/PDF-only deliverable
• pure icon UI with labels < 3 words
✅ Pros (6)
• Visual hierarchy becomes automatic — designers can't accidentally pick a font-size that breaks the rhythm;
• Math-driven ratio means you can add a new size (text-6xl) by multiplying, not guessing;
• Tailwind/utility users get the ramp for free — text-sm/base/lg/xl/2xl matches the scale;
• Line-height couples to font-size (tight for display, loose for body) — readability stays correct at every step;
• Type tokens survive brand pivots: change ratio, regenerate ramp, ship in 30 min instead of 3 days;
• Accessibility audits key off font-size only — semantic tokens let a11y scripts verify 16px minimum automatically.
⚠️ Cons (5)
• 1.25 (Major Third) feels tight at large sizes; 1.5 feels airy — one ratio doesn't serve display + body;
• Body must stay ≥ 16px or hit iOS auto-zoom on focus — ratios that start at 14px fail mobile;
• Variable fonts can collapse the ramp into one weight axis — adopting scales duplicates what's already there;
• Line-length (measure) is more important than size — a perfect scale with 200-char lines still fails reading tests;
• Asian/CJK fonts have wider footprints — Latin scale numbers look cramped on Thai/Chinese body text.
⚡ Gotchas (8)
• Headings < 16px on mobile trigger iOS Safari auto-zoom on input focus — keep input text ≥ 16px even if h6 is smaller;
• rem scales with user browser font setting — a 1.5 ratio on a user-set 20px base ≠ the design; test with browser zoom 200%;
• font-family swap (FOUT) reflows text — reserve width with `size-adjust` + `ascent-override` @font-face descriptors;
• Variable font weight axis (wght) needs explicit `font-variation-settings: 'wght' 600` — `font-weight: 600` alone may not work;
• Letter-spacing tightens on display sizes, loosens on captions — apply per token (`--tracking-tight: -0.02em`);
• Korean/Thai have no ligatures — `font-feature-settings: 'liga' 1` enables complex shaping only where supported;
• Don't mix serif display + sans body without a reason — two families need 2× the work in variable font licensing;
• Tabular numerals (`font-variant-numeric: tabular-nums`) matter for prices, timers, tables — sans tables look jittery on resize.
🚫 Anti-patterns
• Using random pixel values (13px, 17px, 23px) — kills rhythm and confuses the next dev who has to extend it;
• Picking a scale ratio without testing display sizes — 1.25 makes text-6xl too small for hero headlines;
• Setting body < 16px on mobile — iOS Safari auto-zooms inputs and breaks layout;
• Mixing units (px + rem + em) in one scale — rem for everything is the 2024 default;
• Ignoring measure (line-length): 80 chars max for body, 30-40 for captions — perfect scale with bad measure still reads badly.
🔀 Alternatives
vs Tailwind text-* utilities (built-in 1.25 ramp)
vs Type Scale (type-scale.com) for visual math
vs Utopia.fyi (fluid clamp() generator)
📚 Prereqs: 1.2
➡️ Next: 2.4, 2.5
💻 Minimal (159 chars) ▶ Full example (789 chars)
:root { --text-xs:.75rem; --text-sm:.875rem; --text-base:1rem; --text-lg:1.25rem; --text-xl:1.5rem; --text-2xl:1.875rem; --text-3xl:2.25rem; --text-4xl:3rem; }
:root {\n  /* ratio 1.250 — Major Third, base 1rem (16px) */\n  --text-xs: 0.75rem;     /* 12px */\n  --text-sm: 0.875rem;    /* 14px — captions only, never body */\n  --text-base: 1rem;      /* 16px — body min for mobile */\n  --text-lg: 1.25rem;     /* 20px */\n  --text-xl: 1.563rem;    /* 25px */\n  --text-2xl: 1.953rem;   /* 31px */\n  --text-3xl: 2.441rem;   /* 39px */\n  --text-4xl: 3.052rem;   /* 49px */\n  --leading-tight: 1.15;  /* display */\n  --leading-normal: 1.55; /* body */\n  --tracking-tight: -0.02em;\n}\nh1 { font-size: var(--text-4xl); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); }\nbody { font-size: var(--text-base); line-height: var(--leading-normal); }\ninput, select, textarea { font-size: max(1rem, 16px); } /* block iOS zoom */
typographyscaletypetype-rampอักษรฟอนต์modular
2.3

📏 Spacing Tokens (8pt Grid)

Beginner⏱ 15 min

All margin/padding/gap values from a fixed scale (4/8/12/16/24/32/48/64/96) so every gap is divisible by 4 — pixel-snaps cleanly on retina and aligns across teams.

🎯 When to use: Multi-dev team; design system; pixel-perfect mockups in Figma; want pixel-perfect comparison to design files
⛔ When NOT to use
• Single-dev weekend prototype
• canvas/3D game where units are world-space not screen-space
• pixel-art where 1px = 1 unit
✅ Pros (6)
• All values divisible by 4 → pixel-snaps to whole pixels on 1x, 2x, 3x displays (no fuzzy 1.5px gaps);
• Dev ↔ designer parity: Figma '8pt grid' plugin produces the exact same token names as CSS variables;
• Conversion to rem (16px base) becomes trivial: --space-4:1rem; --space-8:2rem; user-zoom respects scale;
• Visual rhythm reads as intentional, not accidental — every gap has a reason because the scale is finite;
• Tailwind's 4/8/16/24/32/48 ramp matches this 1:1 — migrating tokens to utilities is zero-work;
• Catches rogue 13px / 17px / 23px paddings in code review — lint rule `margin: ! \d+(px)` rejects them.
⚠️ Cons (5)
• 8px is too coarse for tight UI (button icon gap of 6px would round to 4 or 8 and feel wrong);
• Half-step tokens (--space-1: 4px, --space-2: 8px) still leak — some teams need 6px for icon-text baselines;
• Some components naturally break the grid (1px borders, 0.5px hairlines) — tokens don't replace those;
• Figma grids are 8pt but engineering reality includes iOS safe-area insets (34pt) that are off-grid;
• Adopting on top of an existing codebase means sed-replacing 1000+ literals — migration cost is real.
⚡ Gotchas (8)
• Padding inside small buttons (< 32px tall) needs --space-2 (8px), not --space-4 (16px) — the scale serves size, not just visual rhythm;
• iOS safe-area-inset-* (notch, home indicator) returns 34pt / 21pt — off-grid by design; wrap in `env(safe-area-inset-bottom)` not raw token;
• border-radius follows its own scale (--radius-sm/md/lg/pill) — coupling it to spacing makes pills wrong (9999px ≠ 64);
• Stacked gaps with margin collapse: use `gap` on flex/grid (no collapse) instead of margin + padding pairs;
• Negative margins for overlapping layouts (`margin-left: -16px`) need negative tokens (`--space-neg-4: -1rem`) or raw values;
• Letter-spacing creates optical spacing — don't tighten scale to compensate for tracked text;
• Charts: bar gap ≠ card gap ≠ list-item gap — domain-specific scale (--chart-bar-gap: 4px) sometimes beats global;
• Vertical rhythm via `line-height: 1.5 * font-size` should hit grid — at 16px base, 24px line = --space-6, perfect alignment.
🚫 Anti-patterns
• Random 13/17/23 values — the whole point of the scale is to forbid these;
• One token per layout (`--hero-padding: 87px`) — bespoke values break the system;
• Mixing px and rem in one scale — pick rem (user-zoom-aware) and commit;
• Defining --space-3, --space-5, --space-7 to fill gaps — every gap is a decision you have to defend;
• Skipping radius/elevation tokens because 'spacing is enough' — those are separate scales that share the grid concept.
🔀 Alternatives
vs Tailwind 4/8/16/24/32 default scale
vs Material Design 4dp/8dp grid
vs Bootstrap 5 spacing utilities
📚 Prereqs: 1.2
➡️ Next: 2.4, 2.10
💻 Minimal (144 chars) ▶ Full example (586 chars)
:root { --space-1:4px; --space-2:8px; --space-4:16px; --space-6:24px; --space-8:32px; --space-12:48px; --radius-md:12px; --radius-pill:9999px; }
:root {\n  --space-0: 0;\n  --space-1: 0.25rem;  /*  4px */\n  --space-2: 0.5rem;   /*  8px */\n  --space-3: 0.75rem;  /* 12px */\n  --space-4: 1rem;     /* 16px */\n  --space-6: 1.5rem;   /* 24px */\n  --space-8: 2rem;     /* 32px */\n  --space-12: 3rem;    /* 48px */\n  --space-16: 4rem;    /* 64px */\n  --radius-sm: 6px;\n  --radius-md: 12px;\n  --radius-lg: 20px;\n  --radius-pill: 9999px;\n}\n.card { padding: var(--space-4); border-radius: var(--radius-md); }\n.btn { padding: var(--space-2) var(--space-4); gap: var(--space-2); }\n.stack > * + * { margin-top: var(--space-4); }
spacinggrid8ptrhythmระยะกริดtokens
2.4

🧱 Component Library

Intermediate⏱ 4 hours

Build Button, Card, Modal, Input, Toast, Chip, Tabs, Tooltip once with variants (size/state/elevation) and reuse — atoms become the team's shared vocabulary.

🎯 When to use: App > 3 pages; team ≥ 2 devs; any project that ships v2+; you find yourself copy-pasting the same button class
⛔ When NOT to use
• Single-page marketing site
• one-shot landing page
• prototype that lives < 1 week
• you have < 10 components total
✅ Pros (6)
• Write button once, use 1000× — accessibility (focus ring, ARIA, keyboard) lives in one place, not 47 places;
• Visual regression tests (Percy/Chromatic) lock one Button.png — every change is reviewed against baseline;
• Onboarding new devs: teach 8 components, they ship features; don't teach 800 CSS rules;
• Bug fix in one component fixes every screen — saves 10× the work a fragmented codebase costs;
• Storybook/Histoire dev mode gives QA/designers a sandbox without engineering deploying;
• Theme/variant via data-attributes (data-variant=danger, data-size=sm) — JS reads CSS, not the other way around.
⚠️ Cons (5)
• Up-front cost: 4–8 hours per component is normal before any page uses it — feels unproductive;
• Over-abstraction: a 'universal' Form component that handles 12 use-cases becomes a 600-line monster nobody dares touch;
• Variant explosion: Button × 5 sizes × 5 variants × 3 states = 75 CSS permutations to maintain;
• Lock-in to choices made early (border vs shadow elevation) — pivoting is a 2-day rewrite, not a 1-hour swap;
• Component-only thinking misses composition — a Modal is fine but a Modal+Toast+Form might have layout bugs you never tested.
⚡ Gotchas (8)
• Use native `<dialog>` for modals — gets focus trap, ESC-to-close, backdrop for free; `showModal()` + `::backdrop` CSS;
• Toast notifications need an aria-live region (`role=status` or `aria-live=polite`) — without it, screen readers miss them;
• Tabs use `role=tablist` + `role=tab` + `aria-selected` + arrow-key navigation — manual is 30 lines, library is 1 import;
• Inputs need explicit `<label for>` — placeholder-as-label fails accessibility audits and disappears on focus;
• Disabled buttons (`disabled`) lose focusability — for 'loading' states use `aria-busy=true` + visually disable but keep focusable;
• `<button>` vs `<a>`: use button for actions (no URL change), anchor for navigation (right-click → new tab works);
• Focus rings: never `outline: none` without replacement — `:focus-visible` ring with 2px solid + offset is the modern default;
• Shadow elevation: keep depth consistent — 1 layer of shadow + border beats 3 nested shadows that look muddy on retina.
🚫 Anti-patterns
• Component-per-pixel-figma: one component per mockup without extracting shared atoms (3 button styles become 3 components that should be 1 with variants);
• Wrapper-only components: `<Card><Card.Body><Card.Title>` when a plain `<div class=card>` with utility classes is enough;
• JS-driven everything: a button that needs JS to toggle a class is a `<details>` or checkbox under the hood;
• Inconsistent naming: Btn/Button/PrimaryButton in same codebase — pick one (Button is the React-y default) and lint;
• No prop API contract: component accepts 12 string props that should be 2 enums — over-flexibility is over-bug-surface.
🔀 Alternatives
vs Headless UI / Radix (unstyled accessible primitives)
vs shadcn/ui (copy-paste component recipes)
vs Storybook (visual dev environment for any lib)
📚 Prereqs: 2.1, 2.3
➡️ Next: 2.5, 2.6
💻 Minimal (168 chars) ▶ Full example (1016 chars)
.card { background:var(--color-surface); border-radius:var(--radius-lg); padding:var(--space-4); transition:transform .2s; } .card:hover { transform:translateY(-2px); }
/* Button component with variants via data-attribute */\n.btn {\n  display: inline-flex; align-items: center; gap: var(--space-2);\n  padding: var(--space-2) var(--space-4);\n  border: 1px solid transparent; border-radius: var(--radius-md);\n  font: inherit; font-weight: 600; cursor: pointer;\n  transition: background .15s, transform .1s;\n}\n.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n.btn[data-variant=\"primary\"] { background: var(--color-accent); color: white; }\n.btn[data-variant=\"ghost\"] { background: transparent; color: var(--color-text-primary); }\n.btn[data-size=\"sm\"] { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }\n.btn[aria-busy=\"true\"] { opacity: .6; cursor: progress; }\n/* Modal via native <dialog> */\ndialog { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-6); background: var(--color-surface); }\ndialog::backdrop { background: rgb(0 0 0 / 60%); backdrop-filter: blur(4px); }
componentslibrarydesign-systemuiatomicชิ้นส่วน
2.5

✨ Animation Library (Easing + Duration Tokens)

Intermediate⏱ 1 hour

Tokenize easing curves (ease-out for enter, ease-in for exit) and durations (100/200/300/500ms) so every motion across the app feels consistent and respects prefers-reduced-motion.

🎯 When to use: UI feels static; want premium feel; need visual feedback on hover/click; modals/sheets/toasts in/out
⛔ When NOT to use
• Decoration only (bouncing logos that add no information)
• users with `prefers-reduced-motion: reduce`
• data-dense dashboards where motion hides info
• 60fps gameplay (use rAF, not CSS)
✅ Pros (6)
• GPU-accelerated transforms (translate/scale/rotate) + opacity hit 60fps even on mid-range Android — width/height/margin animations don't;
• Tokens for duration/easing mean every component speaks the same motion language — no awkward 173ms transitions;
• `prefers-reduced-motion` media query is 1 line to disable — a11y audit passes, vestibular-disorder users are safe;
• Easing > duration for perceived quality: same 250ms with `cubic-bezier(0.16,1,0.3,1)` feels 2× more premium than `ease`;
• Web Animations API (WAAPI) lets you pause/seek/reverse without state libraries — `element.animate(keyframes, options)`;
• Reduced motion fallback (duration: 0.01ms) doesn't lose state — opacity:0 element still appears, just instantly.
⚠️ Cons (5)
• Bouncy animations on primary CTAs delay action by 200ms — every delay is a UX tax on users in a hurry;
• backdrop-filter + transform combo on Safari can drop to 15fps — test on real iPhone, not just Mac Safari;
• Auto-playing animations on page load violate WCAG 2.2.2 — pause/play controls required for > 5s loops;
• CSS @keyframes can't be triggered programmatically without adding/removing classes — JS event → class → animation is brittle;
• Stagger animations (> 5 children with delays) pile up in compositor memory — laptops throttle after ~50 concurrent.
⚡ Gotchas (8)
• Animate `transform` and `opacity` only — they run on the compositor; animating `top/left/width/height` triggers layout every frame;
• `will-change: transform` is a hint, not a guarantee — overuse creates new compositor layers that exhaust GPU memory (200MB+);
• `cubic-bezier(0.16, 1, 0.3, 1)` ('ease-out-expo') for enter, `cubic-bezier(0.7, 0, 0.84, 0)` ('ease-in-quart') for exit — symmetric ease looks mechanical;
• `prefers-reduced-motion: reduce` must set animation-duration to ~0.01ms (not 0 — animation events may not fire at 0);
• iOS Safari pauses CSS animations when tab is backgrounded — resume by listening to `visibilitychange` and re-applying class;
• Transform on SVG elements uses different origin than HTML — `transform-origin: center` works, but nested SVG transforms compound;
• Stagger via `transition-delay: calc(var(--i) * 50ms)` with `--i: 0..n` set inline — cleaner than nth-child arithmetic;
• Scroll-driven animations (CSS Scroll-Linked Animations, 2024+) require `animation-timeline: scroll()` — not yet in Safari stable.
🚫 Anti-patterns
• Animating `box-shadow` for hover effects — triggers paint; animate `background` color or use a pre-rendered shadow layer;
• Same duration for enter and exit (250ms in, 250ms out) — feels slower because user perceives exit first; use 200ms in / 150ms out;
• Long animation chains: button press → ripple → modal open → toast slide → page transition = 1.5s of motion before user sees content;
• Bounce/easing on text (letters wobble) — distracts from reading; reserve motion for spatial changes;
• No reduced-motion fallback — a vestibular-disorder user literally cannot use your app, and an a11y audit will fail.
🔀 Alternatives
vs Framer Motion / Motion One (JS-driven, larger)
vs GSAP (timeline-based, expensive license)
vs Pure CSS @keyframes (zero-dep, what we recommend)
📚 Prereqs: 2.4
💻 Minimal (214 chars) ▶ Full example (863 chars)
:root { --ease-out:cubic-bezier(0.16,1,0.3,1); --duration-normal:250ms; } @media (prefers-reduced-motion:reduce) { *,*::before,*::after { transition-duration:.01ms!important; animation-duration:.01ms!important; } }
:root {\n  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);     /* enter — fast start, soft land */\n  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);      /* exit — slow start, fast leave */\n  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* spatial movement */\n  --duration-fast: 150ms;\n  --duration-normal: 250ms;\n  --duration-slow: 400ms;\n}\n@keyframes bounce-in {\n  0%   { opacity: 0; transform: scale(0.92); }\n  60%  { transform: scale(1.04); }\n  100% { opacity: 1; transform: scale(1); }\n}\n.modal { animation: bounce-in var(--duration-normal) var(--ease-out) both; }\n.btn:active { transform: scale(0.97); transition: transform 80ms var(--ease-in); }\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after {\n    animation-duration: 0.01ms !important;\n    transition-duration: 0.01ms !important;\n    scroll-behavior: auto !important;\n  }\n}
animationmotioneasingdurationprefers-reduced-motionอนิเมชันเคลื่อนไหว
2.6

🎯 Icon System (Emoji + SVG)

Beginner⏱ 15 min

Pick emoji for zero-byte speed or inline SVG for crisp controllable icons — never both in one UI; currentColor in SVG makes them themable with no extra classes.

🎯 When to use: Every UI; nav menus; status indicators; button labels; social/CTA icons
⛔ When NOT to use
• Brand-specific icons that match a system (Material Symbols, Phosphor, Heroicons) — those bring consistency you don't want to fight
• pure emoji-only apps where OS variance is acceptable
✅ Pros (6)
• Emoji cost 0 bytes and inherit system font (no font-load blocking) — fastest possible icon;
• Inline SVG with `fill='currentColor'` or `stroke='currentColor'` inherits text color — theme switching just works;
• SVG scales to any size without raster artifacts; emoji at 96px+ looks pixely on some OS;
• SVG `<path>` data is text — grep-able, version-controllable, optimizable via SVGO;
• Single SVG sprite (1 HTTP request) reused via `<svg><use href=#icon-home>` beats 50 icon requests;
• Stroke-width consistency (1.5 / 2) across an icon family reads as designed-by-one-hand; mixing weights looks amateur.
⚠️ Cons (5)
• Emoji render differently across OS — 🍑 on iOS ≠ Windows ≠ Android (color emoji vs text); brand suffers;
• Inline SVG bloats HTML — 30 icons inline = 30KB in DOM (sprite + use is 30KB once, then 100B per use);
• Emoji in <input> or <button> text aligns weirdly — `vertical-align: -0.1em` fixes most cases;
• Lucide/Phosphor add 50–200KB — for 10 icons, hand-rolled inline SVG is smaller;
• Emoji accessibility: screen readers read '🎉' as 'party popper' which is rarely the intent — wrap with `<span aria-label>`.
⚡ Gotchas (8)
• Apple Color Emoji vs Segoe UI Emoji vs Noto Color Emoji — same codepoint renders 3 different glyphs; test on each;
• SVG `width=20 height=20` attributes are CSS-overridable but the SVG won't resize if it has a viewBox without preserveAspectRatio tweak;
• `<svg>` inside `<button>` is fine, but the SVG must have `aria-hidden=true` so screen readers don't announce 'image';
• Stroke alignment: 24×24 grid with 2px stroke at center means visual weight differs from 2px stroke on 16×16; scale icons together;
• Some emoji have hidden variation selectors (️ U+FE0F) that change text → emoji presentation — copy-paste from emoji keyboards, not codepoints;
• Animated SVG icons (`<animate>` element inside SVG) trigger compositor differently than CSS — sometimes jankier on iOS;
• Icon font ligatures (`:before { content: '\e001' }`) lose to inline SVG on accessibility — no text fallback if font fails;
• `<use href='#icon'>` requires CORS-friendly URLs for cross-origin SVGs — same-origin sprites work without headers.
🚫 Anti-patterns
• Mixing emoji + SVG in the same UI (🎵 for play but SVG for pause) — visual chaos; pick one and commit;
• Icon font (FontAwesome) for 5 icons when inline SVG is 1KB total — 80KB of font for 5 glyphs is wasteful;
• Random stroke widths (1px outline icon next to 3px filled icon) — pick a weight (1.5 / 2) and apply via stroke-width CSS var;
• Color-locking icons (`fill='#000'`) — theming breaks; use `fill='currentColor'` and let CSS cascade;
• Emoji-as-button: `👍` to 'like' is OS-dependent and screenshotted differently by every user — design a button, not an emoji vote.
🔀 Alternatives
vs Lucide (1500+ SVG icons, tree-shakeable)
vs Phosphor Icons (6 weights, flexible family)
vs Heroicons (Tailwind-made, MIT)
vs Material Symbols (Google, variable axes)
📚 Prereqs: 1.1
➡️ Next: 2.4
💻 Minimal (130 chars) ▶ Full example (754 chars)
<svg width=20 height=20 viewBox='0 0 24 24' fill='none' stroke=currentColor stroke-width=2><path d='M12 5v14M5 12l7 7 7-7'/></svg>
<!-- icons.svg — single sprite, fetch once and cache forever -->\n<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display:none\">\n  <symbol id=\"i-home\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path d=\"M3 12l9-9 9 9M5 10v10h14V10\"/>\n  </symbol>\n  <symbol id=\"i-search\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n    <circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/>\n  </symbol>\n</svg>\n<!-- usage: -->\n<button class=\"btn\">\n  <svg class=\"icon\" aria-hidden=\"true\"><use href=\"#i-search\"/></svg>\n  <span>Search</span>\n</button>\n<style>.icon { width: 1.25em; height: 1.25em; }</style>
iconssvgemojispriteไอคอน
2.7

📱 Responsive Breakpoints (Mobile-first)

Intermediate⏱ 1 hour

Define min-width breakpoints (640/768/1024/1280) and write styles for mobile, then layer on `min-width` overrides — desktop styles inherit unless contradicted.

🎯 When to use: Public-facing app; mobile traffic > 30%; SaaS that signs up users on phones; any B2C product
⛔ When NOT to use
• Internal tool desktop-only
• admin dashboard restricted to ops staff on workstations
• kiosk/embedded UI
✅ Pros (6)
• Mobile-first forces performance discipline — base CSS is what phones download; desktop gets progressive enhancement;
• min-width queries are easier to reason about: 'at 768px AND UP, show 2 columns' beats 'at < 1024px, show 1 column';
• Tailwind/Bootstrap defaults match this (sm/md/lg/xl/2xl) — no custom breakpoint debate;
• Container queries (2023+) extend this to components — `.card { @container (min-width: 400px) { flex-direction: row } }`;
• One CSS file works across 320px iPhone SE → 4K monitor — no UA sniffing, no separate m.example.com;
• Touch targets ≥ 44×44 px (Apple HIG) become a checklist item at mobile breakpoint, not an afterthought.
⚠️ Cons (5)
• min-width cascades can hide desktop bugs — dev laptop at 1280px sees both, but a 1366px user sees overflow;
• Tablet portrait (768px) is awkward — too narrow for desktop, too wide for phone layout;
• Container queries need polyfill on iOS 15 (works in 16+) — feature-detect with @supports;
• Hover-only interactions (`:hover` reveals menu) break on touch — must pair with `:focus-within` or click;
• Bandwidth cost: base mobile CSS is small, but if you ship desktop images with `display:none` on mobile, they still download.
⚡ Gotchas (8)
• Viewport meta tag: `<meta name=viewport content='width=device-width, initial-scale=1'>` — without it, mobile browsers render at 980px and zoom out;
• iOS Safari 100vh = URL bar height + bottom bar — use `100dvh` (dynamic viewport) or `min-height: calc(100vh - env(safe-area-inset-bottom))`;
• Touch target size: 44×44 px minimum (Apple) / 48×48 dp (Android) — buttons that LOOK 32px but have larger padding still fail if click area is small;
• `<input>` font-size < 16px triggers iOS auto-zoom on focus — even on a desktop viewport in mobile Safari;
• Landscape phone (568px height) breaks 100vh layouts — test both orientations, not just portrait;
• Landscape iPad split-view (320px / 1024px) needs container queries, not viewport media queries;
• Hover states on touch devices get 'sticky' (tap shows hover, doesn't dismiss until tap elsewhere) — `@media (hover: hover)` gates them;
• Save-data header (`@media (prefers-reduced-data: reduce)`) lets users opt out of heavy assets — serve 1× images instead of 2×;
🚫 Anti-patterns
• Desktop-first (max-width) queries — reverses the cascade and adds cognitive load;
• Custom breakpoints that don't match Tailwind/Bootstrap (sm=620 instead of 640) — engineers will guess wrong;
• Hiding content with `display:none` instead of `loading=lazy` + responsive images — wastes bandwidth on phones;
• Designing for one device (iPhone 14 Pro) — test on cheapest Android in your analytics top-5 countries;
• Forgetting landscape phone — 568px tall × 100vh header + footer + content = 0 scrollable pixels.
🔀 Alternatives
vs Tailwind sm/md/lg/xl/2xl (utility-driven)
vs Bootstrap breakpoints (5 standard steps)
vs Container Queries (component-level, 2023+)
📚 Prereqs: 2.3
➡️ Next: 2.10
💻 Minimal (223 chars) ▶ Full example (859 chars)
.grid { display:grid; gap:var(--space-4); grid-template-columns:1fr; } @media (min-width:640px) { .grid { grid-template-columns:repeat(2,1fr); } } @media (min-width:1024px) { .grid { grid-template-columns:repeat(4,1fr); } }
/* Mobile-first: base = 1 column phone, scale up */\n.grid {\n  display: grid;\n  gap: var(--space-4);\n  grid-template-columns: 1fr;\n  padding: var(--space-4);\n}\n@media (min-width: 640px)  { .grid { grid-template-columns: repeat(2, 1fr); padding: var(--space-6); } }\n@media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); padding: var(--space-8); } }\n/* Touch-friendly targets */\n.btn { min-height: 44px; min-width: 44px; padding: var(--space-3) var(--space-4); }\n/* Hover only where hover exists */\n@media (hover: hover) {\n  .card:hover { transform: translateY(-2px); }\n}\n/* Viewport meta — required */\n/* <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\"> */\n.safe-top    { padding-top:    env(safe-area-inset-top); }\n.safe-bottom { padding-bottom: env(safe-area-inset-bottom); }
responsivemobilebreakpointsmedia-queriesmobile-firstมือถือเบรกพอยต์
2.8

🌙 Dark Mode (prefers-color-scheme + Manual Toggle)

Intermediate⏱ 1 hour

Read system preference via `prefers-color-scheme`, let user override via toggle, persist choice in localStorage — three sources of truth must be reconciled in order: explicit > saved > system.

🎯 When to use: Long-session apps (editor, dashboard, chat); users read in bed; OLED phones (battery savings); professional tool users expect it
⛔ When NOT to use
• Brand-mandatory colors (banking trust colors, medical UI)
• print-primary deliverables
• one-shot landing
✅ Pros (6)
• OLED phones save 30–60% battery on true-black UI — measurable drain reduction;
• Reduces eye strain in low-light reading — users stay in app 25% longer (medium-published 2019 study);
• `prefers-color-scheme` respects OS-level accessibility settings — honors user choice automatically;
• Manual override + localStorage = 3 layers (system, saved, explicit) — UI always shows what user picked;
• Implementation is mostly CSS — `@media (prefers-color-scheme: dark) { :root { ... } }` works in all modern browsers;
• Dark mode often reveals bad contrast — building it forces an a11y audit you should have done anyway.
⚠️ Cons (5)
• Every color must be defined twice (light + dark) — 2× design work, 2× QA surface, 2× screenshot tests;
• Chart libraries (Chart.js, D3) hardcode colors — read CSS vars at chart creation, but redraw on theme change is non-trivial;
• Images baked in light mode look wrong in dark — need `prefers-color-scheme` swap, filter inversion, or two assets;
• Form controls have OS-specific styles — `color-scheme: dark` is needed to make selects/inputs render dark on iOS;
• Pure #000 / pure #fff fail the 'looks right' test — true design requires off-black (#0a0a14) and off-white (#f5f5f7).
⚡ Gotchas (8)
• `color-scheme: dark light` on `:root` makes form controls, scrollbars, and `<input type=date>` pick up dark — without it, iOS Safari shows white inputs on dark bg;
• Pure `#000` looks like a hole — use `hsl(240 10% 5%)` or `#0a0a14`; pure `#fff` is harsh on eyes — `#f5f5f7` is friendlier;
• Shadows are invisible on dark — switch to borders or colored glow (`box-shadow: 0 0 0 1px rgba(255,255,255,.1)`) for elevation in dark mode;
• Images: `<picture>` with `<source media=(prefers-color-scheme: dark) srcset=dark.png>` swaps assets; CSS `filter: invert(1) hue-rotate(180deg)` is hackier but 0 extra HTTP;
• System preference can change at runtime (OS theme switch) — listen with `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...)`;
• Persist explicit user choice in localStorage with key `theme`; resolve order: localStorage > system > fallback light;
• Flash of wrong theme (FOWT) on page load — inline a tiny `<script>` in `<head>` that sets `data-theme` before CSS parses;
• Charts using canvas need to be redrawn when theme changes — listen for theme-change custom event and call chart.update();
🚫 Anti-patterns
• Auto-detect only (no manual toggle) — users on shared computers can't override; respect their preference;
• Using pure #000 — looks like a void; use off-black for depth;
• Two completely different palettes (light = blue, dark = orange) — defeats the purpose of theming; keep semantic roles consistent;
• Forgetting to test form controls — iOS Safari defaults to white inputs on dark bg unless `color-scheme: dark`;
• Persisting in sessionStorage — survives the session, vanishes on tab close; localStorage is the right tier for theme.
🔀 Alternatives
vs next-themes (React)
vs Theme UI (JS-driven theming)
vs Manual CSS-only (what we recommend for vanilla projects)
📚 Prereqs: 1.2, 2.1
💻 Minimal (225 chars) ▶ Full example (1105 chars)
@media (prefers-color-scheme:dark) { :root { --bg:#0a0a14; --text:#f5f5f7; } } [data-theme=dark] { --bg:#0a0a14; --text:#f5f5f7; } const setTheme=t=>{document.documentElement.dataset.theme=t;localStorage.setItem('theme',t);};
/* === Inline script in <head> to prevent flash of wrong theme === */\n<script>\n  (function() {\n    const saved = localStorage.getItem('theme');\n    const sys = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n    const theme = saved || sys;\n    document.documentElement.dataset.theme = theme;\n  })();\n</script>\n\n/* === CSS === */\n:root { color-scheme: light dark; --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\n@media (prefers-color-scheme: dark) {\n  :root { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\n}\n[data-theme=\"dark\"]  { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\n[data-theme=\"light\"] { --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\n\n/* === Toggle handler === */\nconst setTheme = t => {\n  document.documentElement.dataset.theme = t;\n  localStorage.setItem('theme', t);\n  window.dispatchEvent(new CustomEvent('themechange', { detail: t }));\n};\n// listen for OS theme change\nmatchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\n  if (!localStorage.getItem('theme')) setTheme(e.matches ? 'dark' : 'light');\n});
dark-modethemeprefers-color-schemeoledโหมดมืดธีม
2.9

💎 Glassmorphism (Frosted Glass Overlays)

Beginner⏱ 20 min

Use `backdrop-filter: blur()` + semi-transparent background + 1px white border for premium frosted-glass panels that hint at content behind — a Material/iOS design staple.

🎯 When to use: Overlays on busy backgrounds (3D scene, video, photo); iOS-style sheets/modals; hero sections with content underneath; premium product positioning
⛔ When NOT to use
• Print-friendly pages
• form-heavy input flows where contrast matters for legibility
• low-end devices (Android Go, 4-year-old phones)
• text-heavy content where readability > style
✅ Pros (6)
• Implies depth/hierarchy without literal shadows — UI reads as layered, modern, intentional;
• `backdrop-filter: blur()` runs on GPU (compositor layer) — keeps main thread free for app logic;
• Border highlight (1px rgba(255,255,255,.18)) sells the glass illusion more than blur radius does;
• Native iOS / macOS use this exact pattern — users have trained visual literacy;
• Content behind tells users 'there's more here' — better than opaque modals that trap focus;
• Pairs naturally with dark mode (10% white over dark = subtle, premium feel).
⚠️ Cons (5)
• Performance: `backdrop-filter` triggers a new compositor layer per element — 20 glass panels = 20 layers, GPU memory spikes;
• Low-end Android (pre-2020 Mali GPUs) can drop to 5–10 fps when 5+ glass elements are visible;
• Contrast nightmare: white-on-light-photo text disappears; always pair with text-shadow or text on a separate dark band;
• Safari needs `-webkit-backdrop-filter` AND modern syntax — older Safari (pre-18) renders wrong;
• Print styles: `backdrop-filter` is invisible in PDF/print — explicit fallback bg color required for `@media print`.
⚡ Gotchas (8)
• Always declare `-webkit-backdrop-filter` before `backdrop-filter` — Safari < 18 ignores unprefixed; `-moz` was never needed;
• Blur radius sweet spot is 8–16px; > 24px looks fake (gaussian stack overflows) and tanks perf;
• Background opacity must be 5–15% on dark bg, 40–60% on light bg — too transparent = invisible, too opaque = not glass;
• Add `border: 1px solid rgba(255,255,255,.18)` — without it, glass edge looks ragged against varying backgrounds;
• iOS notch + safe-area: glass sheet must include `padding-bottom: env(safe-area-inset-bottom)` or home indicator overlaps;
• `backdrop-filter` doesn't blur elements ABOVE the glass — only those below in stacking order;
• Detect low-end devices: `@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; background: var(--color-surface); } }` (Safari);
• Inside a transformed parent (`transform: translateZ(0)`), backdrop-filter can fail entirely — promote to its own layer;
🚫 Anti-patterns
• Glass over glass (nested panels) — multiple compositor layers stack and slow to single-digit fps;
• Glass containing dense text without a contrast layer — a11y fails on WCAG 1.4.3 (4.5:1);
• Blur radius 40+ — looks like an Instagram filter, not a design choice;
• Mixing glass with flat opaque panels in same UI — visual inconsistency, looks accidental;
• Applying glass to entire body bg — wipes GPU memory on first paint, scroll stutters for 3 seconds.
🔀 Alternatives
vs Solid card with shadow (cheaper, more accessible)
vs Frosted image (manual gaussian via filter:blur)
vs Native iOS UIVisualEffectView (Apple-only)
📚 Prereqs: 2.4
💻 Minimal (146 chars) ▶ Full example (719 chars)
.glass { background:rgba(255,255,255,.1); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,.2); }
.glass {\n  background: rgba(255, 255, 255, 0.08);\n  backdrop-filter: blur(12px) saturate(180%);\n  -webkit-backdrop-filter: blur(12px) saturate(180%);\n  border: 1px solid rgba(255, 255, 255, 0.18);\n  border-radius: var(--radius-lg);\n  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\n}\n.glass-dark {\n  background: rgba(10, 10, 20, 0.55);\n  color: hsl(0 0% 96%);\n}\n/* Low-end / accessibility fallback */\n@media (prefers-reduced-transparency: reduce) {\n  .glass {\n    backdrop-filter: none;\n    -webkit-backdrop-filter: none;\n    background: var(--color-surface);\n  }\n}\n/* Safe-area aware sheet */\n.sheet {\n  padding: var(--space-6);\n  padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));\n}
glassglassmorphismblurbackdrop-filterfrostedกระจกฝ้า
2.10

📐 Layout Primitives (Grid, Flex, Stack, Cluster)

Beginner⏱ 1 hour

Compose layouts from 4 named primitives — Grid (2D), Flex (1D), Stack (vertical w/ consistent gap), Cluster (horizontal wrap) — instead of inventing per-component magic numbers.

🎯 When to use: Every layout; any time you're tempted to write `position: absolute` to align two things; you want RTL support for free
⛔ When NOT to use
• Single absolutely-positioned element (1-off)
• canvas/WebGL scenes where layout is in world-space
• print stylesheet
✅ Pros (6)
• Grid for 2D, Flex for 1D — picking the right one upfront saves hours of `justify-content: center` guessing;
• `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` is responsive without a single media query — 1 to N columns automatically;
• Stack primitive (`> * + * { margin-top: var(--gap) }`) gives vertical rhythm with zero magic numbers;
• Container queries (`@container`) make components responsive to their parent, not viewport — cards in sidebars vs main column both work;
• Grid + flex are RTL-ready by default (`direction: rtl` flips everything) — no `transform: scaleX(-1)` hacks;
• aspect-ratio (2021+) eliminates padding-bottom hack for 16:9 / 1:1 / 4:3 containers — `aspect-ratio: 16/9;` and done.
⚠️ Cons (5)
• Grid + flex syntax has a learning curve — `place-items: center` vs `justify-items + align-items` confuses new devs;
• Container queries (2023+) need fallback for older Safari — `@supports (container-type: inline-size)` gates the rule;
• Subgrid (Firefox-first, partial Chrome/Safari 2024+) isn't universal — nested grids can't share tracks reliably yet;
• Stack via `> * + * { margin-top }` doesn't work with pseudo-elements that already have margin — manual override needed;
• IE 11 doesn't support grid at all — irrelevant for modern apps, but legacy enterprise clients will break.
⚡ Gotchas (8)
• `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` collapses empty tracks — use `auto-fill` if you want fixed columns even when empty;
• Flex `gap` (2021+) replaces margin-between-children — but `gap` on flexbox needs Chromium 84+, Safari 14.1+;
• `place-items: center` is shorthand for `align-items + justify-items` — saves 14 chars, reads better;
• Container queries need `container-type: inline-size` on parent + `@container (min-width: 400px)` on child; without the type declaration, nothing works;
• Stack via `> * + * { margin-top }` only applies between adjacent children — pseudo-elements like `::before` still get the margin if they exist;
• `min-height: 100dvh` (dynamic viewport) handles mobile URL-bar collapse better than `100vh` — supported in iOS Safari 15.4+;
• `aspect-ratio` overrides `width`/`height` if both are set — set width:100% and aspect-ratio, not width+height;
• Negative margins on grid items can leak outside the container — use `overflow: hidden` on parent or grid gaps > 0;
🚫 Anti-patterns
• Float-based layouts (legacy `float: left`) — never; flex/grid subsume every float use case;
• `position: absolute` for centering — flex `place-items: center` is 1 line and respects content size;
• Nested flex inside flex inside flex (> 4 deep) — refactor to grid or extract a primitive;
• Magic numbers (`margin-top: 17px` 'to make it line up') — every magic number is a future bug;
• Setting both `width` and `height` on an image inside flex — use `object-fit: cover` and let aspect-ratio handle the container.
🔀 Alternatives
vs Flexbox (1D only)
vs CSS Grid (2D, what we use)
vs Container Queries (component-responsive)
vs Subgrid (Firefox-first, partial)
📚 Prereqs: 2.3
💻 Minimal (148 chars) ▶ Full example (1249 chars)
.grid { display:grid; gap:var(--space-4); grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); } .stack > * + * { margin-top: var(--space-4); }
/* === Grid: 2D responsive layout, no media queries === */\n.grid {\n  display: grid;\n  gap: var(--space-4);\n  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n}\n.grid-3 { grid-template-columns: repeat(3, 1fr); }\n\n/* === Flex: 1D horizontal/vertical alignment === */\n.row    { display: flex; gap: var(--space-4); align-items: center; }\n.row-end { display: flex; gap: var(--space-4); justify-content: flex-end; }\n\n/* === Stack: vertical rhythm with consistent gap === */\n.stack > * + * { margin-top: var(--space-4); }\n.stack-sm > * + * { margin-top: var(--space-2); }\n.stack-lg > * + * { margin-top: var(--space-8); }\n\n/* === Cluster: wrap-on-overflow horizontal === */\n.cluster {\n  display: flex; flex-wrap: wrap; gap: var(--space-3);\n  align-items: center; justify-content: flex-start;\n}\n\n/* === Aspect-ratio: 16:9 video, 1:1 avatar, 4:3 card === */\n.media-16x9 { aspect-ratio: 16 / 9; width: 100%; object-fit: cover; }\n.avatar      { aspect-ratio: 1; border-radius: 50%; }\n\n/* === Container queries: component-responsive, not viewport === */\n.card-container { container-type: inline-size; }\n@container (min-width: 400px) {\n  .card { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); }\n}
layoutgridflexcontainer-queriesstackเลย์เอาต์

🤖 AI-Readable JSON for this phase

All 10 sub-phases of Design System as JSON — perfect for AI agents.

{
  "phase": {
    "id": 2,
    "name": "Design System",
    "icon": "🎨",
    "tagline": "Visual language for consistent UI",
    "color": "#a78bfa",
    "slug": "design-system"
  },
  "sub_phases": [
    {
      "id": "2.1",
      "title": "Color Tokens (Semantic Palette)",
      "icon": "🎨",
      "description": "Map visual colors (gray-500, blue-600) to semantic roles (text-primary, surface-raised) so theme switching and white-labeling become data-attribute changes.",
      "when_to_use": "Multi-theme product; white-label SaaS; design system spanning > 1 app; you need dark mode; brand refresh every 6 months",
      "when_not_to_use": "Single-page landing with 5 elements; one-off marketing site; prototype that ships once and dies; no theming requirement",
      "pros": [
        "Theme switch = 1-line CSS change (`data-theme=dark`) — zero JS, zero rebuild;",
        "Word 'primary' survives a brand refresh where 'blue' would force a 200-file rename;",
        "WCAG contrast checks happen against semantic roles, not raw values — 1 audit covers every theme;",
        "White-label fork from 100 → 1 codebase: client picks theme JSON at deploy time;",
        "Designers and devs speak the same vocabulary in Figma Variables + code (token name parity);",
        "Accessibility audits become mechanical: scan token→pair pairs, not 5000 hex literals."
      ],
      "cons": [
        "Indirection tax — juniors grep 'primary' and miss the actual value;",
        "Token overlap ('accent' vs 'brand') confuses teams if not enforced in lint;",
        "HSL/oklch math is required to derive shades from one base — wrong math = ugly ramps;",
        "Tailwind already does this — adopting tokens on top adds a layer you must maintain;",
        "Print/email fallbacks still need raw hex — tokens leak into non-browser contexts."
      ],
      "gotchas": [
        "Pure #000 on pure #fff hits WCAG AAA but feels harsh — use #0a0a14 / #f5f5f7 for softer contrast that still passes AA (4.5:1);",
        "HSL hue rotation for variant shades shifts color identity — use oklch or HSL with locked hue; rotate lightness only for a true ramp;",
        "Form controls (input, select, checkbox) ignore CSS background by default — explicit `accent-color` and `color-scheme: dark` are required;",
        "iOS Safari auto-applies blue/yellow tint to inputs in dark mode unless `color-scheme: dark` is set on `:root`;",
        "Images baked in light mode look like flashlights in dark — apply `filter: invert(1) hue-rotate(180deg)` to <img> or use SVG;",
        "Charts (Chart.js, D3) hardcode colors — read tokens via getComputedStyle, don't import CSS into JS;",
        "Transparent tokens (`--surface: hsl(... / 50%)`) need a separate `--surface-solid` for accessibility overlays or backgrounds stack visibly;",
        "Shadow tokens don't theme — shadows look wrong on dark bg unless `--shadow-color` is also a token (alpha-included)."
      ],
      "anti_patterns": [
        "Naming tokens by color: `--blue-500` instead of `--color-primary-500` — they become useless the moment brand changes;",
        "Defining 50 shades when the ramp needs 5 (50/100/200/500/900) — every shade is a future decision you forgot;",
        "Storing tokens in JS only — sync drift between Figma and code; use CSS custom props or Style Dictionary;",
        "Using hex when HSL/oklch would let you derive variants with one knob (lightness);",
        "Skipping the 'danger/warning/info/success' family because 'the app never shows errors' — it will, in week 3."
      ],
      "prereqs": [
        "1.2"
      ],
      "next_steps": [
        "2.2",
        "2.5",
        "2.8"
      ],
      "examples": [
        "codehub-homepage",
        "flowboard",
        "stronghold-3d",
        "habit-streak"
      ],
      "snippet": ":root { --color-bg:hsl(240 10% 5%); --color-text-primary:hsl(0 0% 96%); --color-accent:hsl(330 81% 60%); }",
      "snippet_full": ":root {\\n  --color-bg: hsl(240 10% 5%);\\n  --color-surface: hsl(240 10% 9%);\\n  --color-text-primary: hsl(0 0% 96%);\\n  --color-text-muted: hsl(240 5% 65%);\\n  --color-accent: hsl(330 81% 60%);\\n  --color-success: hsl(142 71% 45%);\\n  --color-warning: hsl(38 92% 50%);\\n  --color-danger: hsl(0 84% 60%);\\n  --color-border: hsl(240 6% 20%);\\n}\\n[data-theme=\\\"light\\\"] {\\n  --color-bg: hsl(0 0% 98%);\\n  --color-surface: hsl(0 0% 100%);\\n  --color-text-primary: hsl(240 10% 10%);\\n  --color-text-muted: hsl(240 5% 40%);\\n}\\n:root { color-scheme: dark light; }\\n.btn { background: var(--color-accent); color: var(--color-text-primary); border: 1px solid var(--color-border); }",
      "tags": [
        "color",
        "tokens",
        "wcag",
        "design-system",
        "theme",
        "สี",
        "ธีม"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "1 hour",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/--*",
      "compare_with": [
        "1.2 (raw CSS variables without semantic naming)",
        "Tailwind config (built-in but JS-bound)",
        "Style Dictionary (multi-platform export)"
      ]
    },
    {
      "id": "2.2",
      "title": "Typography Scale (Modular)",
      "icon": "🔤",
      "description": "Pick a ratio (1.250 / 1.333 / 1.414 / 1.5) and generate a type ramp xs→5xl with consistent visual rhythm — stops decision fatigue and survives redesigns.",
      "when_to_use": "UI with > 3 distinct text sizes; design system spanning multiple pages; need consistent hierarchy in marketing + product UI; brand guidelines exist",
      "when_not_to_use": "Single landing with hero + body + footer; print/PDF-only deliverable; pure icon UI with labels < 3 words",
      "pros": [
        "Visual hierarchy becomes automatic — designers can't accidentally pick a font-size that breaks the rhythm;",
        "Math-driven ratio means you can add a new size (text-6xl) by multiplying, not guessing;",
        "Tailwind/utility users get the ramp for free — text-sm/base/lg/xl/2xl matches the scale;",
        "Line-height couples to font-size (tight for display, loose for body) — readability stays correct at every step;",
        "Type tokens survive brand pivots: change ratio, regenerate ramp, ship in 30 min instead of 3 days;",
        "Accessibility audits key off font-size only — semantic tokens let a11y scripts verify 16px minimum automatically."
      ],
      "cons": [
        "1.25 (Major Third) feels tight at large sizes; 1.5 feels airy — one ratio doesn't serve display + body;",
        "Body must stay ≥ 16px or hit iOS auto-zoom on focus — ratios that start at 14px fail mobile;",
        "Variable fonts can collapse the ramp into one weight axis — adopting scales duplicates what's already there;",
        "Line-length (measure) is more important than size — a perfect scale with 200-char lines still fails reading tests;",
        "Asian/CJK fonts have wider footprints — Latin scale numbers look cramped on Thai/Chinese body text."
      ],
      "gotchas": [
        "Headings < 16px on mobile trigger iOS Safari auto-zoom on input focus — keep input text ≥ 16px even if h6 is smaller;",
        "rem scales with user browser font setting — a 1.5 ratio on a user-set 20px base ≠ the design; test with browser zoom 200%;",
        "font-family swap (FOUT) reflows text — reserve width with `size-adjust` + `ascent-override` @font-face descriptors;",
        "Variable font weight axis (wght) needs explicit `font-variation-settings: 'wght' 600` — `font-weight: 600` alone may not work;",
        "Letter-spacing tightens on display sizes, loosens on captions — apply per token (`--tracking-tight: -0.02em`);",
        "Korean/Thai have no ligatures — `font-feature-settings: 'liga' 1` enables complex shaping only where supported;",
        "Don't mix serif display + sans body without a reason — two families need 2× the work in variable font licensing;",
        "Tabular numerals (`font-variant-numeric: tabular-nums`) matter for prices, timers, tables — sans tables look jittery on resize."
      ],
      "anti_patterns": [
        "Using random pixel values (13px, 17px, 23px) — kills rhythm and confuses the next dev who has to extend it;",
        "Picking a scale ratio without testing display sizes — 1.25 makes text-6xl too small for hero headlines;",
        "Setting body < 16px on mobile — iOS Safari auto-zooms inputs and breaks layout;",
        "Mixing units (px + rem + em) in one scale — rem for everything is the 2024 default;",
        "Ignoring measure (line-length): 80 chars max for body, 30-40 for captions — perfect scale with bad measure still reads badly."
      ],
      "prereqs": [
        "1.2"
      ],
      "next_steps": [
        "2.4",
        "2.5"
      ],
      "examples": [
        "stronghold-3d",
        "karaoke-studio",
        "codehub-homepage",
        "knowledge-garden"
      ],
      "snippet": ":root { --text-xs:.75rem; --text-sm:.875rem; --text-base:1rem; --text-lg:1.25rem; --text-xl:1.5rem; --text-2xl:1.875rem; --text-3xl:2.25rem; --text-4xl:3rem; }",
      "snippet_full": ":root {\\n  /* ratio 1.250 — Major Third, base 1rem (16px) */\\n  --text-xs: 0.75rem;     /* 12px */\\n  --text-sm: 0.875rem;    /* 14px — captions only, never body */\\n  --text-base: 1rem;      /* 16px — body min for mobile */\\n  --text-lg: 1.25rem;     /* 20px */\\n  --text-xl: 1.563rem;    /* 25px */\\n  --text-2xl: 1.953rem;   /* 31px */\\n  --text-3xl: 2.441rem;   /* 39px */\\n  --text-4xl: 3.052rem;   /* 49px */\\n  --leading-tight: 1.15;  /* display */\\n  --leading-normal: 1.55; /* body */\\n  --tracking-tight: -0.02em;\\n}\\nh1 { font-size: var(--text-4xl); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); }\\nbody { font-size: var(--text-base); line-height: var(--leading-normal); }\\ninput, select, textarea { font-size: max(1rem, 16px); } /* block iOS zoom */",
      "tags": [
        "typography",
        "scale",
        "type",
        "type-ramp",
        "อักษร",
        "ฟอนต์",
        "modular"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "30 min",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/font-size",
      "compare_with": [
        "Tailwind text-* utilities (built-in 1.25 ramp)",
        "Type Scale (type-scale.com) for visual math",
        "Utopia.fyi (fluid clamp() generator)"
      ]
    },
    {
      "id": "2.3",
      "title": "Spacing Tokens (8pt Grid)",
      "icon": "📏",
      "description": "All margin/padding/gap values from a fixed scale (4/8/12/16/24/32/48/64/96) so every gap is divisible by 4 — pixel-snaps cleanly on retina and aligns across teams.",
      "when_to_use": "Multi-dev team; design system; pixel-perfect mockups in Figma; want pixel-perfect comparison to design files",
      "when_not_to_use": "Single-dev weekend prototype; canvas/3D game where units are world-space not screen-space; pixel-art where 1px = 1 unit",
      "pros": [
        "All values divisible by 4 → pixel-snaps to whole pixels on 1x, 2x, 3x displays (no fuzzy 1.5px gaps);",
        "Dev ↔ designer parity: Figma '8pt grid' plugin produces the exact same token names as CSS variables;",
        "Conversion to rem (16px base) becomes trivial: --space-4:1rem; --space-8:2rem; user-zoom respects scale;",
        "Visual rhythm reads as intentional, not accidental — every gap has a reason because the scale is finite;",
        "Tailwind's 4/8/16/24/32/48 ramp matches this 1:1 — migrating tokens to utilities is zero-work;",
        "Catches rogue 13px / 17px / 23px paddings in code review — lint rule `margin: ! \\d+(px)` rejects them."
      ],
      "cons": [
        "8px is too coarse for tight UI (button icon gap of 6px would round to 4 or 8 and feel wrong);",
        "Half-step tokens (--space-1: 4px, --space-2: 8px) still leak — some teams need 6px for icon-text baselines;",
        "Some components naturally break the grid (1px borders, 0.5px hairlines) — tokens don't replace those;",
        "Figma grids are 8pt but engineering reality includes iOS safe-area insets (34pt) that are off-grid;",
        "Adopting on top of an existing codebase means sed-replacing 1000+ literals — migration cost is real."
      ],
      "gotchas": [
        "Padding inside small buttons (< 32px tall) needs --space-2 (8px), not --space-4 (16px) — the scale serves size, not just visual rhythm;",
        "iOS safe-area-inset-* (notch, home indicator) returns 34pt / 21pt — off-grid by design; wrap in `env(safe-area-inset-bottom)` not raw token;",
        "border-radius follows its own scale (--radius-sm/md/lg/pill) — coupling it to spacing makes pills wrong (9999px ≠ 64);",
        "Stacked gaps with margin collapse: use `gap` on flex/grid (no collapse) instead of margin + padding pairs;",
        "Negative margins for overlapping layouts (`margin-left: -16px`) need negative tokens (`--space-neg-4: -1rem`) or raw values;",
        "Letter-spacing creates optical spacing — don't tighten scale to compensate for tracked text;",
        "Charts: bar gap ≠ card gap ≠ list-item gap — domain-specific scale (--chart-bar-gap: 4px) sometimes beats global;",
        "Vertical rhythm via `line-height: 1.5 * font-size` should hit grid — at 16px base, 24px line = --space-6, perfect alignment."
      ],
      "anti_patterns": [
        "Random 13/17/23 values — the whole point of the scale is to forbid these;",
        "One token per layout (`--hero-padding: 87px`) — bespoke values break the system;",
        "Mixing px and rem in one scale — pick rem (user-zoom-aware) and commit;",
        "Defining --space-3, --space-5, --space-7 to fill gaps — every gap is a decision you have to defend;",
        "Skipping radius/elevation tokens because 'spacing is enough' — those are separate scales that share the grid concept."
      ],
      "prereqs": [
        "1.2"
      ],
      "next_steps": [
        "2.4",
        "2.10"
      ],
      "examples": [
        "codehub-homepage",
        "stronghold-3d",
        "flowboard"
      ],
      "snippet": ":root { --space-1:4px; --space-2:8px; --space-4:16px; --space-6:24px; --space-8:32px; --space-12:48px; --radius-md:12px; --radius-pill:9999px; }",
      "snippet_full": ":root {\\n  --space-0: 0;\\n  --space-1: 0.25rem;  /*  4px */\\n  --space-2: 0.5rem;   /*  8px */\\n  --space-3: 0.75rem;  /* 12px */\\n  --space-4: 1rem;     /* 16px */\\n  --space-6: 1.5rem;   /* 24px */\\n  --space-8: 2rem;     /* 32px */\\n  --space-12: 3rem;    /* 48px */\\n  --space-16: 4rem;    /* 64px */\\n  --radius-sm: 6px;\\n  --radius-md: 12px;\\n  --radius-lg: 20px;\\n  --radius-pill: 9999px;\\n}\\n.card { padding: var(--space-4); border-radius: var(--radius-md); }\\n.btn { padding: var(--space-2) var(--space-4); gap: var(--space-2); }\\n.stack > * + * { margin-top: var(--space-4); }",
      "tags": [
        "spacing",
        "grid",
        "8pt",
        "rhythm",
        "ระยะ",
        "กริด",
        "tokens"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "15 min",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/padding",
      "compare_with": [
        "Tailwind 4/8/16/24/32 default scale",
        "Material Design 4dp/8dp grid",
        "Bootstrap 5 spacing utilities"
      ]
    },
    {
      "id": "2.4",
      "title": "Component Library",
      "icon": "🧱",
      "description": "Build Button, Card, Modal, Input, Toast, Chip, Tabs, Tooltip once with variants (size/state/elevation) and reuse — atoms become the team's shared vocabulary.",
      "when_to_use": "App > 3 pages; team ≥ 2 devs; any project that ships v2+; you find yourself copy-pasting the same button class",
      "when_not_to_use": "Single-page marketing site; one-shot landing page; prototype that lives < 1 week; you have < 10 components total",
      "pros": [
        "Write button once, use 1000× — accessibility (focus ring, ARIA, keyboard) lives in one place, not 47 places;",
        "Visual regression tests (Percy/Chromatic) lock one Button.png — every change is reviewed against baseline;",
        "Onboarding new devs: teach 8 components, they ship features; don't teach 800 CSS rules;",
        "Bug fix in one component fixes every screen — saves 10× the work a fragmented codebase costs;",
        "Storybook/Histoire dev mode gives QA/designers a sandbox without engineering deploying;",
        "Theme/variant via data-attributes (data-variant=danger, data-size=sm) — JS reads CSS, not the other way around."
      ],
      "cons": [
        "Up-front cost: 4–8 hours per component is normal before any page uses it — feels unproductive;",
        "Over-abstraction: a 'universal' Form component that handles 12 use-cases becomes a 600-line monster nobody dares touch;",
        "Variant explosion: Button × 5 sizes × 5 variants × 3 states = 75 CSS permutations to maintain;",
        "Lock-in to choices made early (border vs shadow elevation) — pivoting is a 2-day rewrite, not a 1-hour swap;",
        "Component-only thinking misses composition — a Modal is fine but a Modal+Toast+Form might have layout bugs you never tested."
      ],
      "gotchas": [
        "Use native `<dialog>` for modals — gets focus trap, ESC-to-close, backdrop for free; `showModal()` + `::backdrop` CSS;",
        "Toast notifications need an aria-live region (`role=status` or `aria-live=polite`) — without it, screen readers miss them;",
        "Tabs use `role=tablist` + `role=tab` + `aria-selected` + arrow-key navigation — manual is 30 lines, library is 1 import;",
        "Inputs need explicit `<label for>` — placeholder-as-label fails accessibility audits and disappears on focus;",
        "Disabled buttons (`disabled`) lose focusability — for 'loading' states use `aria-busy=true` + visually disable but keep focusable;",
        "`<button>` vs `<a>`: use button for actions (no URL change), anchor for navigation (right-click → new tab works);",
        "Focus rings: never `outline: none` without replacement — `:focus-visible` ring with 2px solid + offset is the modern default;",
        "Shadow elevation: keep depth consistent — 1 layer of shadow + border beats 3 nested shadows that look muddy on retina."
      ],
      "anti_patterns": [
        "Component-per-pixel-figma: one component per mockup without extracting shared atoms (3 button styles become 3 components that should be 1 with variants);",
        "Wrapper-only components: `<Card><Card.Body><Card.Title>` when a plain `<div class=card>` with utility classes is enough;",
        "JS-driven everything: a button that needs JS to toggle a class is a `<details>` or checkbox under the hood;",
        "Inconsistent naming: Btn/Button/PrimaryButton in same codebase — pick one (Button is the React-y default) and lint;",
        "No prop API contract: component accepts 12 string props that should be 2 enums — over-flexibility is over-bug-surface."
      ],
      "prereqs": [
        "2.1",
        "2.3"
      ],
      "next_steps": [
        "2.5",
        "2.6"
      ],
      "examples": [
        "codehub-homepage",
        "flowboard",
        "knowledge-garden",
        "eventtix"
      ],
      "snippet": ".card { background:var(--color-surface); border-radius:var(--radius-lg); padding:var(--space-4); transition:transform .2s; } .card:hover { transform:translateY(-2px); }",
      "snippet_full": "/* Button component with variants via data-attribute */\\n.btn {\\n  display: inline-flex; align-items: center; gap: var(--space-2);\\n  padding: var(--space-2) var(--space-4);\\n  border: 1px solid transparent; border-radius: var(--radius-md);\\n  font: inherit; font-weight: 600; cursor: pointer;\\n  transition: background .15s, transform .1s;\\n}\\n.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\\n.btn[data-variant=\\\"primary\\\"] { background: var(--color-accent); color: white; }\\n.btn[data-variant=\\\"ghost\\\"] { background: transparent; color: var(--color-text-primary); }\\n.btn[data-size=\\\"sm\\\"] { padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }\\n.btn[aria-busy=\\\"true\\\"] { opacity: .6; cursor: progress; }\\n/* Modal via native <dialog> */\\ndialog { border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-6); background: var(--color-surface); }\\ndialog::backdrop { background: rgb(0 0 0 / 60%); backdrop-filter: blur(4px); }",
      "tags": [
        "components",
        "library",
        "design-system",
        "ui",
        "atomic",
        "ชิ้นส่วน"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "4 hours",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog",
      "compare_with": [
        "Headless UI / Radix (unstyled accessible primitives)",
        "shadcn/ui (copy-paste component recipes)",
        "Storybook (visual dev environment for any lib)"
      ]
    },
    {
      "id": "2.5",
      "title": "Animation Library (Easing + Duration Tokens)",
      "icon": "✨",
      "description": "Tokenize easing curves (ease-out for enter, ease-in for exit) and durations (100/200/300/500ms) so every motion across the app feels consistent and respects prefers-reduced-motion.",
      "when_to_use": "UI feels static; want premium feel; need visual feedback on hover/click; modals/sheets/toasts in/out",
      "when_not_to_use": "Decoration only (bouncing logos that add no information); users with `prefers-reduced-motion: reduce`; data-dense dashboards where motion hides info; 60fps gameplay (use rAF, not CSS)",
      "pros": [
        "GPU-accelerated transforms (translate/scale/rotate) + opacity hit 60fps even on mid-range Android — width/height/margin animations don't;",
        "Tokens for duration/easing mean every component speaks the same motion language — no awkward 173ms transitions;",
        "`prefers-reduced-motion` media query is 1 line to disable — a11y audit passes, vestibular-disorder users are safe;",
        "Easing > duration for perceived quality: same 250ms with `cubic-bezier(0.16,1,0.3,1)` feels 2× more premium than `ease`;",
        "Web Animations API (WAAPI) lets you pause/seek/reverse without state libraries — `element.animate(keyframes, options)`;",
        "Reduced motion fallback (duration: 0.01ms) doesn't lose state — opacity:0 element still appears, just instantly."
      ],
      "cons": [
        "Bouncy animations on primary CTAs delay action by 200ms — every delay is a UX tax on users in a hurry;",
        "backdrop-filter + transform combo on Safari can drop to 15fps — test on real iPhone, not just Mac Safari;",
        "Auto-playing animations on page load violate WCAG 2.2.2 — pause/play controls required for > 5s loops;",
        "CSS @keyframes can't be triggered programmatically without adding/removing classes — JS event → class → animation is brittle;",
        "Stagger animations (> 5 children with delays) pile up in compositor memory — laptops throttle after ~50 concurrent."
      ],
      "gotchas": [
        "Animate `transform` and `opacity` only — they run on the compositor; animating `top/left/width/height` triggers layout every frame;",
        "`will-change: transform` is a hint, not a guarantee — overuse creates new compositor layers that exhaust GPU memory (200MB+);",
        "`cubic-bezier(0.16, 1, 0.3, 1)` ('ease-out-expo') for enter, `cubic-bezier(0.7, 0, 0.84, 0)` ('ease-in-quart') for exit — symmetric ease looks mechanical;",
        "`prefers-reduced-motion: reduce` must set animation-duration to ~0.01ms (not 0 — animation events may not fire at 0);",
        "iOS Safari pauses CSS animations when tab is backgrounded — resume by listening to `visibilitychange` and re-applying class;",
        "Transform on SVG elements uses different origin than HTML — `transform-origin: center` works, but nested SVG transforms compound;",
        "Stagger via `transition-delay: calc(var(--i) * 50ms)` with `--i: 0..n` set inline — cleaner than nth-child arithmetic;",
        "Scroll-driven animations (CSS Scroll-Linked Animations, 2024+) require `animation-timeline: scroll()` — not yet in Safari stable."
      ],
      "anti_patterns": [
        "Animating `box-shadow` for hover effects — triggers paint; animate `background` color or use a pre-rendered shadow layer;",
        "Same duration for enter and exit (250ms in, 250ms out) — feels slower because user perceives exit first; use 200ms in / 150ms out;",
        "Long animation chains: button press → ripple → modal open → toast slide → page transition = 1.5s of motion before user sees content;",
        "Bounce/easing on text (letters wobble) — distracts from reading; reserve motion for spatial changes;",
        "No reduced-motion fallback — a vestibular-disorder user literally cannot use your app, and an a11y audit will fail."
      ],
      "prereqs": [
        "2.4"
      ],
      "next_steps": [],
      "examples": [
        "karaoke-studio",
        "stronghold-3d",
        "eventtix",
        "flowboard"
      ],
      "snippet": ":root { --ease-out:cubic-bezier(0.16,1,0.3,1); --duration-normal:250ms; } @media (prefers-reduced-motion:reduce) { *,*::before,*::after { transition-duration:.01ms!important; animation-duration:.01ms!important; } }",
      "snippet_full": ":root {\\n  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);     /* enter — fast start, soft land */\\n  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);      /* exit — slow start, fast leave */\\n  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* spatial movement */\\n  --duration-fast: 150ms;\\n  --duration-normal: 250ms;\\n  --duration-slow: 400ms;\\n}\\n@keyframes bounce-in {\\n  0%   { opacity: 0; transform: scale(0.92); }\\n  60%  { transform: scale(1.04); }\\n  100% { opacity: 1; transform: scale(1); }\\n}\\n.modal { animation: bounce-in var(--duration-normal) var(--ease-out) both; }\\n.btn:active { transform: scale(0.97); transition: transform 80ms var(--ease-in); }\\n@media (prefers-reduced-motion: reduce) {\\n  *, *::before, *::after {\\n    animation-duration: 0.01ms !important;\\n    transition-duration: 0.01ms !important;\\n    scroll-behavior: auto !important;\\n  }\\n}",
      "tags": [
        "animation",
        "motion",
        "easing",
        "duration",
        "prefers-reduced-motion",
        "อนิเมชัน",
        "เคลื่อนไหว"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "1 hour",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/animation",
      "compare_with": [
        "Framer Motion / Motion One (JS-driven, larger)",
        "GSAP (timeline-based, expensive license)",
        "Pure CSS @keyframes (zero-dep, what we recommend)"
      ]
    },
    {
      "id": "2.6",
      "title": "Icon System (Emoji + SVG)",
      "icon": "🎯",
      "description": "Pick emoji for zero-byte speed or inline SVG for crisp controllable icons — never both in one UI; currentColor in SVG makes them themable with no extra classes.",
      "when_to_use": "Every UI; nav menus; status indicators; button labels; social/CTA icons",
      "when_not_to_use": "Brand-specific icons that match a system (Material Symbols, Phosphor, Heroicons) — those bring consistency you don't want to fight; pure emoji-only apps where OS variance is acceptable",
      "pros": [
        "Emoji cost 0 bytes and inherit system font (no font-load blocking) — fastest possible icon;",
        "Inline SVG with `fill='currentColor'` or `stroke='currentColor'` inherits text color — theme switching just works;",
        "SVG scales to any size without raster artifacts; emoji at 96px+ looks pixely on some OS;",
        "SVG `<path>` data is text — grep-able, version-controllable, optimizable via SVGO;",
        "Single SVG sprite (1 HTTP request) reused via `<svg><use href=#icon-home>` beats 50 icon requests;",
        "Stroke-width consistency (1.5 / 2) across an icon family reads as designed-by-one-hand; mixing weights looks amateur."
      ],
      "cons": [
        "Emoji render differently across OS — 🍑 on iOS ≠ Windows ≠ Android (color emoji vs text); brand suffers;",
        "Inline SVG bloats HTML — 30 icons inline = 30KB in DOM (sprite + use is 30KB once, then 100B per use);",
        "Emoji in <input> or <button> text aligns weirdly — `vertical-align: -0.1em` fixes most cases;",
        "Lucide/Phosphor add 50–200KB — for 10 icons, hand-rolled inline SVG is smaller;",
        "Emoji accessibility: screen readers read '🎉' as 'party popper' which is rarely the intent — wrap with `<span aria-label>`."
      ],
      "gotchas": [
        "Apple Color Emoji vs Segoe UI Emoji vs Noto Color Emoji — same codepoint renders 3 different glyphs; test on each;",
        "SVG `width=20 height=20` attributes are CSS-overridable but the SVG won't resize if it has a viewBox without preserveAspectRatio tweak;",
        "`<svg>` inside `<button>` is fine, but the SVG must have `aria-hidden=true` so screen readers don't announce 'image';",
        "Stroke alignment: 24×24 grid with 2px stroke at center means visual weight differs from 2px stroke on 16×16; scale icons together;",
        "Some emoji have hidden variation selectors (️ U+FE0F) that change text → emoji presentation — copy-paste from emoji keyboards, not codepoints;",
        "Animated SVG icons (`<animate>` element inside SVG) trigger compositor differently than CSS — sometimes jankier on iOS;",
        "Icon font ligatures (`:before { content: '\\e001' }`) lose to inline SVG on accessibility — no text fallback if font fails;",
        "`<use href='#icon'>` requires CORS-friendly URLs for cross-origin SVGs — same-origin sprites work without headers."
      ],
      "anti_patterns": [
        "Mixing emoji + SVG in the same UI (🎵 for play but SVG for pause) — visual chaos; pick one and commit;",
        "Icon font (FontAwesome) for 5 icons when inline SVG is 1KB total — 80KB of font for 5 glyphs is wasteful;",
        "Random stroke widths (1px outline icon next to 3px filled icon) — pick a weight (1.5 / 2) and apply via stroke-width CSS var;",
        "Color-locking icons (`fill='#000'`) — theming breaks; use `fill='currentColor'` and let CSS cascade;",
        "Emoji-as-button: `👍` to 'like' is OS-dependent and screenshotted differently by every user — design a button, not an emoji vote."
      ],
      "prereqs": [
        "1.1"
      ],
      "next_steps": [
        "2.4"
      ],
      "examples": [
        "stronghold-3d",
        "karaoke-studio",
        "flowboard",
        "mood-garden"
      ],
      "snippet": "<svg width=20 height=20 viewBox='0 0 24 24' fill='none' stroke=currentColor stroke-width=2><path d='M12 5v14M5 12l7 7 7-7'/></svg>",
      "snippet_full": "<!-- icons.svg — single sprite, fetch once and cache forever -->\\n<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" style=\\\"display:none\\\">\\n  <symbol id=\\\"i-home\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\">\\n    <path d=\\\"M3 12l9-9 9 9M5 10v10h14V10\\\"/>\\n  </symbol>\\n  <symbol id=\\\"i-search\\\" viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\">\\n    <circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"7\\\"/><path d=\\\"m21 21-4.3-4.3\\\"/>\\n  </symbol>\\n</svg>\\n<!-- usage: -->\\n<button class=\\\"btn\\\">\\n  <svg class=\\\"icon\\\" aria-hidden=\\\"true\\\"><use href=\\\"#i-search\\\"/></svg>\\n  <span>Search</span>\\n</button>\\n<style>.icon { width: 1.25em; height: 1.25em; }</style>",
      "tags": [
        "icons",
        "svg",
        "emoji",
        "sprite",
        "ไอคอน"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "15 min",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use",
      "compare_with": [
        "Lucide (1500+ SVG icons, tree-shakeable)",
        "Phosphor Icons (6 weights, flexible family)",
        "Heroicons (Tailwind-made, MIT)",
        "Material Symbols (Google, variable axes)"
      ]
    },
    {
      "id": "2.7",
      "title": "Responsive Breakpoints (Mobile-first)",
      "icon": "📱",
      "description": "Define min-width breakpoints (640/768/1024/1280) and write styles for mobile, then layer on `min-width` overrides — desktop styles inherit unless contradicted.",
      "when_to_use": "Public-facing app; mobile traffic > 30%; SaaS that signs up users on phones; any B2C product",
      "when_not_to_use": "Internal tool desktop-only; admin dashboard restricted to ops staff on workstations; kiosk/embedded UI",
      "pros": [
        "Mobile-first forces performance discipline — base CSS is what phones download; desktop gets progressive enhancement;",
        "min-width queries are easier to reason about: 'at 768px AND UP, show 2 columns' beats 'at < 1024px, show 1 column';",
        "Tailwind/Bootstrap defaults match this (sm/md/lg/xl/2xl) — no custom breakpoint debate;",
        "Container queries (2023+) extend this to components — `.card { @container (min-width: 400px) { flex-direction: row } }`;",
        "One CSS file works across 320px iPhone SE → 4K monitor — no UA sniffing, no separate m.example.com;",
        "Touch targets ≥ 44×44 px (Apple HIG) become a checklist item at mobile breakpoint, not an afterthought."
      ],
      "cons": [
        "min-width cascades can hide desktop bugs — dev laptop at 1280px sees both, but a 1366px user sees overflow;",
        "Tablet portrait (768px) is awkward — too narrow for desktop, too wide for phone layout;",
        "Container queries need polyfill on iOS 15 (works in 16+) — feature-detect with @supports;",
        "Hover-only interactions (`:hover` reveals menu) break on touch — must pair with `:focus-within` or click;",
        "Bandwidth cost: base mobile CSS is small, but if you ship desktop images with `display:none` on mobile, they still download."
      ],
      "gotchas": [
        "Viewport meta tag: `<meta name=viewport content='width=device-width, initial-scale=1'>` — without it, mobile browsers render at 980px and zoom out;",
        "iOS Safari 100vh = URL bar height + bottom bar — use `100dvh` (dynamic viewport) or `min-height: calc(100vh - env(safe-area-inset-bottom))`;",
        "Touch target size: 44×44 px minimum (Apple) / 48×48 dp (Android) — buttons that LOOK 32px but have larger padding still fail if click area is small;",
        "`<input>` font-size < 16px triggers iOS auto-zoom on focus — even on a desktop viewport in mobile Safari;",
        "Landscape phone (568px height) breaks 100vh layouts — test both orientations, not just portrait;",
        "Landscape iPad split-view (320px / 1024px) needs container queries, not viewport media queries;",
        "Hover states on touch devices get 'sticky' (tap shows hover, doesn't dismiss until tap elsewhere) — `@media (hover: hover)` gates them;",
        "Save-data header (`@media (prefers-reduced-data: reduce)`) lets users opt out of heavy assets — serve 1× images instead of 2×;",
        "1px CSS ≠ 1 device pixel — retina is 3× or 4×; use `image-set()` or `srcset` for bitmap assets."
      ],
      "anti_patterns": [
        "Desktop-first (max-width) queries — reverses the cascade and adds cognitive load;",
        "Custom breakpoints that don't match Tailwind/Bootstrap (sm=620 instead of 640) — engineers will guess wrong;",
        "Hiding content with `display:none` instead of `loading=lazy` + responsive images — wastes bandwidth on phones;",
        "Designing for one device (iPhone 14 Pro) — test on cheapest Android in your analytics top-5 countries;",
        "Forgetting landscape phone — 568px tall × 100vh header + footer + content = 0 scrollable pixels."
      ],
      "prereqs": [
        "2.3"
      ],
      "next_steps": [
        "2.10"
      ],
      "examples": [
        "karaoke-studio",
        "codehub-homepage",
        "flowboard",
        "meal-plan"
      ],
      "snippet": ".grid { display:grid; gap:var(--space-4); grid-template-columns:1fr; } @media (min-width:640px) { .grid { grid-template-columns:repeat(2,1fr); } } @media (min-width:1024px) { .grid { grid-template-columns:repeat(4,1fr); } }",
      "snippet_full": "/* Mobile-first: base = 1 column phone, scale up */\\n.grid {\\n  display: grid;\\n  gap: var(--space-4);\\n  grid-template-columns: 1fr;\\n  padding: var(--space-4);\\n}\\n@media (min-width: 640px)  { .grid { grid-template-columns: repeat(2, 1fr); padding: var(--space-6); } }\\n@media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); padding: var(--space-8); } }\\n/* Touch-friendly targets */\\n.btn { min-height: 44px; min-width: 44px; padding: var(--space-3) var(--space-4); }\\n/* Hover only where hover exists */\\n@media (hover: hover) {\\n  .card:hover { transform: translateY(-2px); }\\n}\\n/* Viewport meta — required */\\n/* <meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1, viewport-fit=cover\\\"> */\\n.safe-top    { padding-top:    env(safe-area-inset-top); }\\n.safe-bottom { padding-bottom: env(safe-area-inset-bottom); }",
      "tags": [
        "responsive",
        "mobile",
        "breakpoints",
        "media-queries",
        "mobile-first",
        "มือถือ",
        "เบรกพอยต์"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "1 hour",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries",
      "compare_with": [
        "Tailwind sm/md/lg/xl/2xl (utility-driven)",
        "Bootstrap breakpoints (5 standard steps)",
        "Container Queries (component-level, 2023+)"
      ]
    },
    {
      "id": "2.8",
      "title": "Dark Mode (prefers-color-scheme + Manual Toggle)",
      "icon": "🌙",
      "description": "Read system preference via `prefers-color-scheme`, let user override via toggle, persist choice in localStorage — three sources of truth must be reconciled in order: explicit > saved > system.",
      "when_to_use": "Long-session apps (editor, dashboard, chat); users read in bed; OLED phones (battery savings); professional tool users expect it",
      "when_not_to_use": "Brand-mandatory colors (banking trust colors, medical UI); print-primary deliverables; one-shot landing",
      "pros": [
        "OLED phones save 30–60% battery on true-black UI — measurable drain reduction;",
        "Reduces eye strain in low-light reading — users stay in app 25% longer (medium-published 2019 study);",
        "`prefers-color-scheme` respects OS-level accessibility settings — honors user choice automatically;",
        "Manual override + localStorage = 3 layers (system, saved, explicit) — UI always shows what user picked;",
        "Implementation is mostly CSS — `@media (prefers-color-scheme: dark) { :root { ... } }` works in all modern browsers;",
        "Dark mode often reveals bad contrast — building it forces an a11y audit you should have done anyway."
      ],
      "cons": [
        "Every color must be defined twice (light + dark) — 2× design work, 2× QA surface, 2× screenshot tests;",
        "Chart libraries (Chart.js, D3) hardcode colors — read CSS vars at chart creation, but redraw on theme change is non-trivial;",
        "Images baked in light mode look wrong in dark — need `prefers-color-scheme` swap, filter inversion, or two assets;",
        "Form controls have OS-specific styles — `color-scheme: dark` is needed to make selects/inputs render dark on iOS;",
        "Pure #000 / pure #fff fail the 'looks right' test — true design requires off-black (#0a0a14) and off-white (#f5f5f7)."
      ],
      "gotchas": [
        "`color-scheme: dark light` on `:root` makes form controls, scrollbars, and `<input type=date>` pick up dark — without it, iOS Safari shows white inputs on dark bg;",
        "Pure `#000` looks like a hole — use `hsl(240 10% 5%)` or `#0a0a14`; pure `#fff` is harsh on eyes — `#f5f5f7` is friendlier;",
        "Shadows are invisible on dark — switch to borders or colored glow (`box-shadow: 0 0 0 1px rgba(255,255,255,.1)`) for elevation in dark mode;",
        "Images: `<picture>` with `<source media=(prefers-color-scheme: dark) srcset=dark.png>` swaps assets; CSS `filter: invert(1) hue-rotate(180deg)` is hackier but 0 extra HTTP;",
        "System preference can change at runtime (OS theme switch) — listen with `matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...)`;",
        "Persist explicit user choice in localStorage with key `theme`; resolve order: localStorage > system > fallback light;",
        "Flash of wrong theme (FOWT) on page load — inline a tiny `<script>` in `<head>` that sets `data-theme` before CSS parses;",
        "Charts using canvas need to be redrawn when theme changes — listen for theme-change custom event and call chart.update();",
        "Code blocks (syntax highlighting) need a separate dark theme — Shiki/Prism both ship light + dark, switch via attribute selector."
      ],
      "anti_patterns": [
        "Auto-detect only (no manual toggle) — users on shared computers can't override; respect their preference;",
        "Using pure #000 — looks like a void; use off-black for depth;",
        "Two completely different palettes (light = blue, dark = orange) — defeats the purpose of theming; keep semantic roles consistent;",
        "Forgetting to test form controls — iOS Safari defaults to white inputs on dark bg unless `color-scheme: dark`;",
        "Persisting in sessionStorage — survives the session, vanishes on tab close; localStorage is the right tier for theme."
      ],
      "prereqs": [
        "1.2",
        "2.1"
      ],
      "next_steps": [],
      "examples": [
        "flowboard",
        "stronghold-3d",
        "codehub-homepage",
        "memo-ai"
      ],
      "snippet": "@media (prefers-color-scheme:dark) { :root { --bg:#0a0a14; --text:#f5f5f7; } } [data-theme=dark] { --bg:#0a0a14; --text:#f5f5f7; } const setTheme=t=>{document.documentElement.dataset.theme=t;localStorage.setItem('theme',t);};",
      "snippet_full": "/* === Inline script in <head> to prevent flash of wrong theme === */\\n<script>\\n  (function() {\\n    const saved = localStorage.getItem('theme');\\n    const sys = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\\n    const theme = saved || sys;\\n    document.documentElement.dataset.theme = theme;\\n  })();\\n</script>\\n\\n/* === CSS === */\\n:root { color-scheme: light dark; --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\\n@media (prefers-color-scheme: dark) {\\n  :root { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\\n}\\n[data-theme=\\\"dark\\\"]  { --bg: hsl(240 10% 5%); --text: hsl(0 0% 96%); }\\n[data-theme=\\\"light\\\"] { --bg: hsl(0 0% 98%); --text: hsl(240 10% 10%); }\\n\\n/* === Toggle handler === */\\nconst setTheme = t => {\\n  document.documentElement.dataset.theme = t;\\n  localStorage.setItem('theme', t);\\n  window.dispatchEvent(new CustomEvent('themechange', { detail: t }));\\n};\\n// listen for OS theme change\\nmatchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\\n  if (!localStorage.getItem('theme')) setTheme(e.matches ? 'dark' : 'light');\\n});",
      "tags": [
        "dark-mode",
        "theme",
        "prefers-color-scheme",
        "oled",
        "โหมดมืด",
        "ธีม"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "1 hour",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme",
      "compare_with": [
        "next-themes (React)",
        "Theme UI (JS-driven theming)",
        "Manual CSS-only (what we recommend for vanilla projects)"
      ]
    },
    {
      "id": "2.9",
      "title": "Glassmorphism (Frosted Glass Overlays)",
      "icon": "💎",
      "description": "Use `backdrop-filter: blur()` + semi-transparent background + 1px white border for premium frosted-glass panels that hint at content behind — a Material/iOS design staple.",
      "when_to_use": "Overlays on busy backgrounds (3D scene, video, photo); iOS-style sheets/modals; hero sections with content underneath; premium product positioning",
      "when_not_to_use": "Print-friendly pages; form-heavy input flows where contrast matters for legibility; low-end devices (Android Go, 4-year-old phones); text-heavy content where readability > style",
      "pros": [
        "Implies depth/hierarchy without literal shadows — UI reads as layered, modern, intentional;",
        "`backdrop-filter: blur()` runs on GPU (compositor layer) — keeps main thread free for app logic;",
        "Border highlight (1px rgba(255,255,255,.18)) sells the glass illusion more than blur radius does;",
        "Native iOS / macOS use this exact pattern — users have trained visual literacy;",
        "Content behind tells users 'there's more here' — better than opaque modals that trap focus;",
        "Pairs naturally with dark mode (10% white over dark = subtle, premium feel)."
      ],
      "cons": [
        "Performance: `backdrop-filter` triggers a new compositor layer per element — 20 glass panels = 20 layers, GPU memory spikes;",
        "Low-end Android (pre-2020 Mali GPUs) can drop to 5–10 fps when 5+ glass elements are visible;",
        "Contrast nightmare: white-on-light-photo text disappears; always pair with text-shadow or text on a separate dark band;",
        "Safari needs `-webkit-backdrop-filter` AND modern syntax — older Safari (pre-18) renders wrong;",
        "Print styles: `backdrop-filter` is invisible in PDF/print — explicit fallback bg color required for `@media print`."
      ],
      "gotchas": [
        "Always declare `-webkit-backdrop-filter` before `backdrop-filter` — Safari < 18 ignores unprefixed; `-moz` was never needed;",
        "Blur radius sweet spot is 8–16px; > 24px looks fake (gaussian stack overflows) and tanks perf;",
        "Background opacity must be 5–15% on dark bg, 40–60% on light bg — too transparent = invisible, too opaque = not glass;",
        "Add `border: 1px solid rgba(255,255,255,.18)` — without it, glass edge looks ragged against varying backgrounds;",
        "iOS notch + safe-area: glass sheet must include `padding-bottom: env(safe-area-inset-bottom)` or home indicator overlaps;",
        "`backdrop-filter` doesn't blur elements ABOVE the glass — only those below in stacking order;",
        "Detect low-end devices: `@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; background: var(--color-surface); } }` (Safari);",
        "Inside a transformed parent (`transform: translateZ(0)`), backdrop-filter can fail entirely — promote to its own layer;"
      ],
      "anti_patterns": [
        "Glass over glass (nested panels) — multiple compositor layers stack and slow to single-digit fps;",
        "Glass containing dense text without a contrast layer — a11y fails on WCAG 1.4.3 (4.5:1);",
        "Blur radius 40+ — looks like an Instagram filter, not a design choice;",
        "Mixing glass with flat opaque panels in same UI — visual inconsistency, looks accidental;",
        "Applying glass to entire body bg — wipes GPU memory on first paint, scroll stutters for 3 seconds."
      ],
      "prereqs": [
        "2.4"
      ],
      "next_steps": [],
      "examples": [
        "stronghold-3d",
        "karaoke-studio",
        "sj88-video-editor-pro",
        "eventtix"
      ],
      "snippet": ".glass { background:rgba(255,255,255,.1); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,.2); }",
      "snippet_full": ".glass {\\n  background: rgba(255, 255, 255, 0.08);\\n  backdrop-filter: blur(12px) saturate(180%);\\n  -webkit-backdrop-filter: blur(12px) saturate(180%);\\n  border: 1px solid rgba(255, 255, 255, 0.18);\\n  border-radius: var(--radius-lg);\\n  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);\\n}\\n.glass-dark {\\n  background: rgba(10, 10, 20, 0.55);\\n  color: hsl(0 0% 96%);\\n}\\n/* Low-end / accessibility fallback */\\n@media (prefers-reduced-transparency: reduce) {\\n  .glass {\\n    backdrop-filter: none;\\n    -webkit-backdrop-filter: none;\\n    background: var(--color-surface);\\n  }\\n}\\n/* Safe-area aware sheet */\\n.sheet {\\n  padding: var(--space-6);\\n  padding-bottom: max(var(--space-6), env(safe-area-inset-bottom));\\n}",
      "tags": [
        "glass",
        "glassmorphism",
        "blur",
        "backdrop-filter",
        "frosted",
        "กระจกฝ้า"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "20 min",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter",
      "compare_with": [
        "Solid card with shadow (cheaper, more accessible)",
        "Frosted image (manual gaussian via filter:blur)",
        "Native iOS UIVisualEffectView (Apple-only)"
      ]
    },
    {
      "id": "2.10",
      "title": "Layout Primitives (Grid, Flex, Stack, Cluster)",
      "icon": "📐",
      "description": "Compose layouts from 4 named primitives — Grid (2D), Flex (1D), Stack (vertical w/ consistent gap), Cluster (horizontal wrap) — instead of inventing per-component magic numbers.",
      "when_to_use": "Every layout; any time you're tempted to write `position: absolute` to align two things; you want RTL support for free",
      "when_not_to_use": "Single absolutely-positioned element (1-off); canvas/WebGL scenes where layout is in world-space; print stylesheet",
      "pros": [
        "Grid for 2D, Flex for 1D — picking the right one upfront saves hours of `justify-content: center` guessing;",
        "`grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` is responsive without a single media query — 1 to N columns automatically;",
        "Stack primitive (`> * + * { margin-top: var(--gap) }`) gives vertical rhythm with zero magic numbers;",
        "Container queries (`@container`) make components responsive to their parent, not viewport — cards in sidebars vs main column both work;",
        "Grid + flex are RTL-ready by default (`direction: rtl` flips everything) — no `transform: scaleX(-1)` hacks;",
        "aspect-ratio (2021+) eliminates padding-bottom hack for 16:9 / 1:1 / 4:3 containers — `aspect-ratio: 16/9;` and done."
      ],
      "cons": [
        "Grid + flex syntax has a learning curve — `place-items: center` vs `justify-items + align-items` confuses new devs;",
        "Container queries (2023+) need fallback for older Safari — `@supports (container-type: inline-size)` gates the rule;",
        "Subgrid (Firefox-first, partial Chrome/Safari 2024+) isn't universal — nested grids can't share tracks reliably yet;",
        "Stack via `> * + * { margin-top }` doesn't work with pseudo-elements that already have margin — manual override needed;",
        "IE 11 doesn't support grid at all — irrelevant for modern apps, but legacy enterprise clients will break."
      ],
      "gotchas": [
        "`grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` collapses empty tracks — use `auto-fill` if you want fixed columns even when empty;",
        "Flex `gap` (2021+) replaces margin-between-children — but `gap` on flexbox needs Chromium 84+, Safari 14.1+;",
        "`place-items: center` is shorthand for `align-items + justify-items` — saves 14 chars, reads better;",
        "Container queries need `container-type: inline-size` on parent + `@container (min-width: 400px)` on child; without the type declaration, nothing works;",
        "Stack via `> * + * { margin-top }` only applies between adjacent children — pseudo-elements like `::before` still get the margin if they exist;",
        "`min-height: 100dvh` (dynamic viewport) handles mobile URL-bar collapse better than `100vh` — supported in iOS Safari 15.4+;",
        "`aspect-ratio` overrides `width`/`height` if both are set — set width:100% and aspect-ratio, not width+height;",
        "Negative margins on grid items can leak outside the container — use `overflow: hidden` on parent or grid gaps > 0;",
        "`place-content: center` centers the entire grid within its container (different from `place-items` which centers items within their cells)."
      ],
      "anti_patterns": [
        "Float-based layouts (legacy `float: left`) — never; flex/grid subsume every float use case;",
        "`position: absolute` for centering — flex `place-items: center` is 1 line and respects content size;",
        "Nested flex inside flex inside flex (> 4 deep) — refactor to grid or extract a primitive;",
        "Magic numbers (`margin-top: 17px` 'to make it line up') — every magic number is a future bug;",
        "Setting both `width` and `height` on an image inside flex — use `object-fit: cover` and let aspect-ratio handle the container."
      ],
      "prereqs": [
        "2.3"
      ],
      "next_steps": [],
      "examples": [
        "codehub-homepage",
        "flowboard",
        "knowledge-garden",
        "eventtix"
      ],
      "snippet": ".grid { display:grid; gap:var(--space-4); grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); } .stack > * + * { margin-top: var(--space-4); }",
      "snippet_full": "/* === Grid: 2D responsive layout, no media queries === */\\n.grid {\\n  display: grid;\\n  gap: var(--space-4);\\n  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\\n}\\n.grid-3 { grid-template-columns: repeat(3, 1fr); }\\n\\n/* === Flex: 1D horizontal/vertical alignment === */\\n.row    { display: flex; gap: var(--space-4); align-items: center; }\\n.row-end { display: flex; gap: var(--space-4); justify-content: flex-end; }\\n\\n/* === Stack: vertical rhythm with consistent gap === */\\n.stack > * + * { margin-top: var(--space-4); }\\n.stack-sm > * + * { margin-top: var(--space-2); }\\n.stack-lg > * + * { margin-top: var(--space-8); }\\n\\n/* === Cluster: wrap-on-overflow horizontal === */\\n.cluster {\\n  display: flex; flex-wrap: wrap; gap: var(--space-3);\\n  align-items: center; justify-content: flex-start;\\n}\\n\\n/* === Aspect-ratio: 16:9 video, 1:1 avatar, 4:3 card === */\\n.media-16x9 { aspect-ratio: 16 / 9; width: 100%; object-fit: cover; }\\n.avatar      { aspect-ratio: 1; border-radius: 50%; }\\n\\n/* === Container queries: component-responsive, not viewport === */\\n.card-container { container-type: inline-size; }\\n@container (min-width: 400px) {\\n  .card { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); }\\n}",
      "tags": [
        "layout",
        "grid",
        "flex",
        "container-queries",
        "stack",
        "เลย์เอาต์"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "1 hour",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout",
      "compare_with": [
        "Flexbox (1D only)",
        "CSS Grid (2D, what we use)",
        "Container Queries (component-responsive)",
        "Subgrid (Firefox-first, partial)",
        "CSS `display: contents` (deprecated, accessibility risk)"
      ]
    }
  ]
}
🔗 /api/skill (all 100) 💾 Download design-system.json