experience-and-design-system
Anti-AI-slop design system for distinctive, premium interfaces. Bold typography, dark-first #060610, fluid clamp() type, cascade layers + native nesting + container queries, OKLCH color, @starting-style, View Transitions API, DTCG tokens.
Install
npx skills add https://github.com/heymegabyte/claude-skills/tree/master/10-experience-and-design-system
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
git clone https://github.com/heymegabyte/claude-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole heymegabyte/claude-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
10 — Experience and Design System
Build anti-AI-slop premium interfaces: dark-first OKLCH color, fluid clamp() type, DTCG tokens, View Transitions on every public-facing surface.
Apple Test — after every design: two elements compete → remove one; crowded → add whitespace; busy type → reduce sizes, increase weight contrast; final feel: effortless, inevitable.
CSS Patterns
- Overlay
rgba(0,0,0,0.81)· Text shadow1px 1px 1px rgba(255,255,255,0.333)· Box shadow2px 2px 2px rgba(0,0,0,0.69) - Border-radius 5px interactive, 10px containers (never 0, never pill)
- Hero padding 40px · Max text 720px · Line-height 1.4
- Letter-spacing: 0.4px labels, 0.5px nav, 1px titles, 1.4px CTAs
- CTA uppercase always · Button 700 always · Reference: Linear, Notion, Stripe
Typography
- Body: Sora 400/500 · Headings: Space Grotesk 600/700 · Mono: JetBrains Mono 400/500 · Display: Clash Display 700 (hero only)
- Variable fonts: WOFF2, subset, self-host (never Google Fonts CDN),
font-display:swap - Body min 16px (prefer 18) · Line-height 1.6 body, 1.1-1.2 headings · Letter-spacing -0.02em >2rem · Max 65ch
- Never skip levels. Scale: Minor Third 1.2 general, Perfect Fourth 1.333 marketing
text-wrap:balanceheadings,text-wrap:prettyparagraphs
:root {
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.25rem);
--text-xl: clamp(1.25rem, 1rem + 1.25vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.1rem + 2vw, 2rem);
--text-3xl: clamp(2rem, 1.5rem + 2.5vw, 3rem);
--text-4xl: clamp(2.5rem, 1.5rem + 5vw, 4.5rem);
--text-hero: clamp(3rem, 2rem + 5vw, 6rem);
}
Color (Dark Default)
:root {
color-scheme: light dark;
--bg-primary: #060610; --bg-secondary: #0a0a1a; --bg-tertiary: #121225;
--bg-card: #0f0f1f; --bg-elevated: #1a1a35;
--text-primary: #f0f0f5; --text-secondary: #a0a0b5; --text-muted: #606080;
--accent-cyan: #00E5FF; --accent-blue: #50AAE3; --accent-purple: #8B5CF6;
--gradient-primary: linear-gradient(135deg, #00E5FF, #50AAE3);
--gradient-accent: linear-gradient(135deg, #50AAE3, #8B5CF6);
--border-subtle: rgba(255,255,255,0.06); --border-hover: rgba(255,255,255,0.12);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3); --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.5); --shadow-glow: 0 0 20px rgba(0,229,255,0.15);
}
- Never
#000(use#060610) · Never#fff(use#f0f0f5) - Cyan: primary CTAs · Blue: secondary · Gradients on buttons only · 6% borders · Subtle glow on primary interactive
- Elevation via lightness not shadows: base → surface 1 → surface 2 → surface 3
color-scheme:light dark·data-theme="dark|light"user override ·localStoragepersistence ·prefers-color-schemesystem default · Always provide toggle- OKLCH perceptually uniform ·
color-mix()· Relativeoklch(from var(--brand) l c calc(h + 30))·light-dark()theme-aware - Contrast 4.5:1 normal, 3:1 large/UI (WCAG 2.2 AA) · Target size min 24×24 px (2.5.8) · Focus 2px thick, 3:1 contrast (2.4.13)
CSS Architecture (2026)
@layer reset, base, tokens, components, utilities, overrides;
- Native nesting · Container queries (
container-type:inline-size,@container) ·:has()replaces JS @scopebounded styling · Anchor positioning replaces Floating UI · Scroll-state queries@container scroll-state(stuck: top)(Baseline 2026)- CSS
if()· Typedattr()·sibling-index()/sibling-count()stagger:transition-delay: calc((sibling-index() - 1) * 40ms) appearance:base-selectnative<select>(Chrome 135+) ·@supportsfor progressive enhancement- Baseline 2026:
@scope· Anchor positioning · Scroll-state queries ·@starting-style·interpolate-size: allow-keywords·field-sizing: content·text-wrap: pretty·@property
W3C DTCG Design Tokens (2025.10 Stable)
- JSON
.tokens/.tokens.json, MIMEapplication/design-tokens+json - Token:
$value(required),$type,$description,$deprecated,$extensions - Types: color | dimension | duration | fontFamily | fontWeight | cubicBezier | number + composites (shadow, border, gradient, typography, transition)
- Aliasing:
"$value": "{base.color}"·$refJSON Pointer · Group$typeinheritance ·$rootfor base + variants ·$extendsfor deep merge - Full Display P3, OKLCH, CSS Color Module 4; naming: no
$prefix, no{}/.in names - Tools: Tokens Studio, Style Dictionary, Penpot, Figma
AI-Ready Design Documentation
DESIGN.md: plain-text markdown; sections: Visual Theme, Color Palette, Typography, Spacing + Layout, Components, Elevation- Atomic docs: context-rich units per component with states, variants, props, constraints, a11y, rationale
- MCP servers (Figma Dev Mode MCP) for programmatic access
Layout
- Container 1140px (wide 1400, narrow 720), padding
clamp(1rem,3vw,3rem) - Sections
clamp(4rem,8vw,8rem), border between; gridauto-fit minmax(280px,1fr), 1fr at 768px - Patterns: Hero (full-viewport, centered) · Features (3-col icon+heading+desc) · Alternating (zigzag) · Pricing (3-tier highlighted) · FAQ (accordion) · CTA (full-width dark) · Footer (4-col stack)
- SaaS: Single-CTA 13.5%; Hero / Proof / Features / Demo / Testimonials / Pricing / FAQ / CTA; bento grid for feature showcases
Components
- Cards — bg-card, border-subtle, 12px radius, hover: border-hover + shadow-glow
- Buttons — Primary gradient #060610 text, 600 weight, 8px radius, hover 0.9, active scale(0.98), focus 3px cyan. Secondary: transparent, border, hover cyan
- Nav — sticky,
rgba(6,6,16,0.85),blur(16px) - Forms — bg-secondary, border-subtle, 8px, focus cyan + glow
- PrimeNG — standalone (not NgModule), OnPush on all, lazy-load heavy (DataTable, Editor, Chart), design tokens for theming
Interaction (every interactive element)
cursor:pointer, hover state, focus-visible (3px cyan, 2px offset), active (scale 0.98), transition (0.2s color, 0.1s transform). WCAG 2.2 — min 24×24px targets, focus not obscured by sticky headers, dragging alternatives required, accessible auth.
4-state distinction (NON-NEGOTIABLE)
Every link/button/card MUST visually differ across :default | :hover | :focus-visible | :active — NEVER let two states look identical.
- Default → neutral · Hover → underline-sweep + color shift +
translateY(-1px)· Focus-visible → 3px cyan ring 2px offset (distinct from hover) · Active →scale(0.98)+ immediate color confirm
Audit gate: Playwright cycles each interactive element through 4 states → screenshots → diff ≥3px pixel-difference between adjacent states or fail.
Underline-sweep (text links default)
.underline-hover{position:relative}
.underline-hover::after{content:"";position:absolute;z-index:1;left:51%;right:51%;bottom:0;background:var(--brand-accent);height:1px;transition:left .3s ease-out,right .3s ease-out}
.underline-hover:hover::after,.underline-hover:focus-visible::after{left:0;right:0}
var(--brand-accent) / theme equivalent — never hard-coded #hex.
See submodules: design-tokens.md, build-breaking-rules.md, cinematic-doctrine.md.
Files (claude-skills)
-
build-breaking-rules.md 36.6 KB
--- name: "10 build-breaking experience+design rules" description: "Universal experience/design gates: clickable card/tile deeplinks, detail-card deeplinks, multi-image lightbox + integrated captions, interactive functionality validator, blog/news/portfolio functional taxonomy filters + search + sort + URL-sync, filter-chip inverted active state, mega-menu hover-bridge + triangle-aim, pointer-cursor honesty, lightbox close scroll-position preservation, expandable card no content cropping, search input ≥50ch desktop, full-width visual section breakout, vertical-stack-leverages-full-real-estate (timelines+grids+dossiers never trapped in max-w-3xl prose container), comparison table full-bleed. Migrated verbatim from rules/always.md 2026-05-03." metadata: version: "1.0.0" updated: "2026-05-03" effort: "high" context: "fork" license: "Rutgers" compatibility: claude-code: ">=2.0.0" agentskills: ">=1.0.0" --- # 10 — Build-Breaking Experience + Design Rules Migrated from `~/.claude/rules/always.md` 2026-05-03. ## Every clickable card/tile (***UNIVERSAL — supersedes "tile-as-link" partial***) - (a) Wrap the entire surface in `<a href>` (or `role="link" tabindex=0` with onClick). - (b) `cursor: pointer` on the whole tile (NOT just the inner button). - (c) Hover lift `transform: translateY(-3px)` + accent border glow. - (d) Inner clickable elements (View button, org name, image) ALL point to the SAME deep target — never internal stub. - Validator (`validate-card-deeplinks.mjs`): every `[data-card]` element has matching `href` AND `cursor: pointer` computed-style at 6bp. ## Every detail-card (publication | paper | case-study | person | portfolio item) - MUST enrich with deep-link to canonical authoritative source — DOI/PubMed/arXiv/journal URL/external authority page (NEVER internal route stub when external source exists). - Card body MUST hyperlink the institution/journal/conference/org name to its canonical URL. - Visible action buttons (View, Read Full Paper, Download, Open) MUST point to the deeplink — never to an internal `/portfolio/slug` page that just re-displays the card. - Build gate: research pipeline annotates each card item with `deeplink_url` in `_research.json`; missing deeplink for items typed `publication|paper|case-study` = fail. ## Every multi-image section (***ONE LIGHTBOX GROUP + INTEGRATED CAPTIONS — UNIVERSAL***) - When a section contains ≥2 lightbox-eligible images on the SAME topic, ALL images MUST share ONE `data-gallery="<section-slug>"` ID — never split same-section images across multiple lightbox groups. - Captions: every image carries `{ title: string, description: string, credit?: string, link?: string }` shipped in BOTH the section UI (card overlay or figcaption) AND the lightbox modal (visible bottom strip + `aria-describedby`). - Validator (`validate-lightbox-grouping.mjs`): if section has ≥2 images and ≥2 distinct gallery IDs in same `<section>` ancestor = fail. Every `[data-zoomable]` MUST have `data-caption-title` AND `data-caption-description` = fail otherwise. ## Every interactive feature (***FUNCTIONALITY VALIDATOR — NOT JUST STYLING — UNIVERSAL — BUILD-BREAKING***) - Every interactive UI element (filter chip, tab, accordion, search input, sort dropdown, toggle, modal, lightbox, mega-menu, infinite-scroll trigger, pagination link, "Load more" button) MUST functionally do what its label promises — never render as styled-but-stub UI. - Filter chip "All Posts | News | Events" MUST actually filter the post grid; tab "Overview | Pricing | Reviews" MUST swap visible panel content; search input MUST filter results; sort dropdown MUST reorder children. - Validator (`validate-interactive-functionality.mjs`): Playwright finds every `[data-filter], [role=tab], [aria-controls], [data-search], [data-sort], [data-load-more]`, simulates click/input, snapshots DOM before+after, asserts measurable difference (count change, attribute toggle, content swap, URL hash update); FAIL on any element where `state-after === state-before`. ## Every blog/news/portfolio listing (***FUNCTIONAL TAXONOMY FILTERS + SEARCH + SORT + URL-SYNC — UNIVERSAL — BUILD-BREAKING***) Every page listing ≥2 posts/articles/case-studies/projects/products MUST ship working category filter + tag filter + free-text search + sort-by-date wired to real corpus data. ### Required behaviors 1. **Category click** — filters `[data-post-card]` to that category; updates URL `?category=<slug>`; sets `aria-pressed=true` on active chip; updates "Showing N of M posts" counter; deep-link-on-load reads `?category=<slug>` and pre-applies filter. 2. **Tag click** — same as category but `?tag=<slug>` (multi-tag = comma-separated). 3. **Free-text `[data-search]` input** — filters by title + excerpt + tags (case-insensitive substring) within 150ms debounce; updates URL `?q=<term>`. 4. **Sort dropdown** — reorders by `data-published-at` (newest | oldest | popular when popularity tracked). 5. **Empty-state** — friendly message + "Clear filters" CTA that strips URL params + refreshes listing. 6. **Keyboard + screen-reader** — Tab/Enter/Space/Esc; `aria-controls`, `aria-expanded`, `role=listbox/option`. - Implementation MUST be client-side state reading from a single `posts: Post[]` array — NEVER server round-trip per filter. Categories + tags rendered MUST be the actual unique values from `posts.flatMap(p => p.categories)` deduped — NEVER hand-authored chip lists. - Validator (`validate-blog-filters.mjs`): for every route matching `/blog|/news|/articles|/posts|/press|/portfolio|/projects|/case-studies|/insights|/stories`, Playwright: (a) counts posts before filter; (b) clicks each category chip; (c) asserts post count after click is `<` initial AND `>` 0; (d) asserts URL contains `?category=<slug>`; (e) clicks "All", asserts count returns to initial; (f) types in search box, asserts count drops; (g) reloads with `?category=<slug>`, asserts pre-applied filter state. FAIL on any step where state-after === state-before OR URL doesn't sync OR pre-applied filter ignores query string. ## Every filter-chip - **Inactive state** — outline + accent text on transparent bg. - **Active/selected state** — **INVERTED** (filled accent bg, bg-color text — fg/bg swap), NEVER a different accent color. Selected state must visually FILL — not just shift color. ```css .chip { color: var(--brand-accent); border: 1px solid var(--brand-accent); background: transparent } .chip[aria-pressed="true"], .chip.is-active { color: var(--bg-primary); background: var(--brand-accent); border-color: var(--brand-accent) } ``` ## Every mega-menu / multi-column nav (***HOVER-BRIDGE + TRIANGLE-AIM ALGORITHM — UNIVERSAL — desktop hover-driven nav***) - **Hover-bridge** — invisible `::before` on dropdown panel: `position:absolute; top:-12px; left:0; right:0; height:12px` — fills cursor-traversal corridor. - **Triangle-aim (Bostock 2013)** — JS tracks last 3 mousemove positions; if cursor moves INTO the triangle defined by current cursor + bottom-left + bottom-right of panel, delay close 300ms; if outside triangle, close 150ms. - Open-delay: 100ms; close-delay: 300ms aimed at panel / 150ms aimed away. - Touch (`pointer:coarse`): tap-to-open + tap-outside-to-close, NO hover bridge. - Keyboard: Enter/Space opens; Esc closes; Arrow keys navigate items; Tab moves to next trigger. - Validator (`validate-mega-menu-hover.mjs`): Playwright desktop — hover trigger → move cursor diagonally through gap → assert panel open after 250ms; second run moves cursor away → assert close within 200ms. Template ships `<MegaMenu>` component — never hand-rolled per site. ## Every clickable element (***POINTER-CURSOR HONESTY — UNIVERSAL — BUILD-BREAKING***) - `cursor: pointer` MUST appear on AND ONLY ON elements that actually do something on click. - Forbidden: decorative cards with pointer cursor + no onclick/href; stat-counter blocks; hero illustrations; plain `<p>`/`<div>` text blocks with pointer cursor inherited from parent. - Required: every `<a href>` / `<button>` / `[role=button]` / element with attached onClick handler GETS `cursor: pointer`; every element WITHOUT click behavior gets `cursor: default`. - Edge cases: text input/textarea/select — use native cursors; drag handles — `cursor: grab` → `cursor: grabbing`. - Validator (`validate-pointer-cursor-honesty.mjs`): Playwright at 6bp — for each element with `cursor: pointer`, assert it has `[onclick]` OR `<a href>` ancestor OR `<button>` ancestor OR `[role=button]` OR `[role=link]` OR `[role=tab]` OR `[role=menuitem]` ancestor — FAIL on bare `<p>`/`<div>`/`<span>`/`<section>` with pointer cursor and no click handler. ## Every lightbox close (***SCROLL-POSITION PRESERVATION — UNIVERSAL — BUILD-BREAKING — extends "Every multi-image section"***) - When lightbox modal closes (Esc, X button, backdrop click, swipe-down), the underlying page scroll position MUST snap back to the EXACT scrollY where the user was when they opened the lightbox — never reset to 0. - **On open** — `const restoreY = window.scrollY` AND `document.body.style.cssText = 'position: fixed; top: -' + restoreY + 'px; left: 0; right: 0; overflow-y: scroll'`. - **On close** — `document.body.style.cssText = ''` THEN `window.scrollTo({ top: restoreY, behavior: 'instant' })` (instant, NOT smooth). - Same pattern applies to ANY full-screen modal (mobile menu, command palette, video player). - Validator (`validate-modal-scroll-preservation.mjs`): Playwright opens lightbox/modal at `scrollY=2000`, closes via Esc, asserts `window.scrollY === 2000` within 50ms. ## Every expandable card / "Read More" toggle (***NO CONTENT CROPPING — UNIVERSAL — BUILD-BREAKING***) - Collapsed-by-default text + toggle MUST render the FULL expanded content visibly within the card's expanded box — never crop, never truncate after expand, never require scroll within the card. - **Collapsed** — `max-height: <N>px; overflow: hidden; mask-image: linear-gradient(180deg, #000 70%, transparent 100%)`. - **Expanded** — `max-height: none; overflow: visible; mask-image: none` — let the card grow to fit. - NEVER expanded state with `max-height: 600px; overflow-y: scroll` (creates double-scroll UX). - Animation: transition `max-height` from collapsed value to `scrollHeight + 'px'` then to `none` post-animation via `transitionend`. - Validator (`validate-expandable-card-no-crop.mjs`): Playwright clicks every `[data-expandable]` toggle, samples post-expand `getBoundingClientRect().height` vs `scrollHeight` — if `clientHeight < scrollHeight` after expand = fail. ## Every search input (***MIN VISIBLE WIDTH ≥50ch DESKTOP — UNIVERSAL — BUILD-BREAKING — extends "Every interactive feature"***) - Every `<input type="search">` / `<input data-search>` MUST render at minimum 50 visible characters (`min-width: clamp(20rem, 60vw, 40rem)`) on viewports ≥768px. - On viewports <768px: stack to full width (`width: 100%`) below adjacent filter chips/sort controls — never crammed into a 200px sliver beside chips. - Pattern: filter chips in `flex-wrap` container with `gap: 0.5rem`; search input as separate row with `width: clamp(20rem, 100%, 40rem)` and `font-size: 1rem` (≥16px on iOS to prevent input zoom); search results dropdown MUST be at least as wide as the input. - Validator (`validate-search-input-width.mjs`): Playwright at 1280px — fail if `[type=search], [data-search]` bounding-rect width `<320px`; at 375px — fail if input shares row with chips. ## Every full-width visual section (***FULL-VIEWPORT BREAKOUT FOR HERO/MEDIA — UNIVERSAL — BUILD-BREAKING***) - Text-content sections: constrained `max-width: 65ch / 1100px / clamp(min, content, 1280px)` with side gutters. - Visual-impact sections (hero, image gallery, full-bleed video, immersive dividers, comparison-table, data-grid): MUST break out to full viewport width (`100vw`, edge-to-edge, no side gutters). - Outer `<main>` has `padding-inline: clamp(1rem, 4vw, 3rem)`. Full-bleed sections use `margin-inline: calc(50% - 50vw); padding-inline: clamp(1rem, 4vw, 3rem)` to escape parent gutters. - Hero images — full-bleed by default. Galleries with ≥6 images — full-bleed grid. Wide data-tables / comparison matrices — full-bleed when content needs >1100px. Text paragraphs / blog body — stay constrained at 65ch. - Validator (`validate-full-bleed-sections.mjs`): fail if hero `<section>` width < `100vw` (allowing 1px scrollbar tolerance); fail if `<table data-comparison>` overflows container instead of breaking out; fail if `<p>` text-block exceeds 75ch. ## Every vertical-stack interactive section (***LEVERAGE FULL SCREEN REAL-ESTATE — UNIVERSAL — BUILD-BREAKING — extends "Every full-width visual section"***) - When a section renders content that stacks vertically (timelines with alternating left/right cards, dossier/spotlight components, multi-card grids ≥6 items, stat-roll-up bars, partner-logo strips, pastor/team grids, founding-facts dl grids, photo galleries ≥4 items, comparison tables, FAQ accordions ≥6 entries, leadership/board grids) the OUTER container MUST be `max-w-6xl` / `max-w-7xl` / full-bleed — NEVER `max-w-3xl` / `max-w-prose` / `max-w-4xl`. - `max-w-3xl` is reserved EXCLUSIVELY for body prose (≤65ch reading line). Any structured/interactive surface — even on a prose-heavy page — MUST break out of the prose container. - Pattern: sibling `<section>` elements each with their own `max-w-*` based on content type: prose `<section className="max-w-3xl">`, structured `<section className="max-w-7xl">`, immersive media full-bleed. Do NOT wrap all content in a single `max-w-3xl`. - Component-level heading inside a `max-w-7xl` section MAY use an inner `<div className="max-w-3xl">` for the heading copy — the interactive surface below it spans the full container. - Validator (`validate-vertical-stack-width.mjs`): Playwright at 1280px finds `[data-timeline], [data-stat-rollup], [data-partner-strip], [data-pastor-grid], ol.grid, dl.grid, [data-dossier], details.accordion` — assert each parent `<section>` `width >= 1024px`. Fail code: `vertical-stack.constrained-by-prose-container`. ## Every site at every viewport (***FULL-SCREEN REAL-ESTATE UTILIZATION — UNIVERSAL — BUILD-BREAKING — supersedes hard pixel `max-width` shells***) - Every generated site at every viewport from 320px through 3840px MUST consume available screen real-estate — no dead margins on ultrawide, no cinematic content trapped in narrow prose columns, no single 600px column on a 1024px tablet. - Outer container token: `--container: min(1680px, calc(100vw - clamp(1rem, 4vw, 3rem)))`. Ultrawide bumps: `@media (min-width: 1920px) { --container: min(1820px, calc(100vw - 4rem)); }` / `@media (min-width: 2400px) { --container: min(2000px, calc(100vw - 6rem)); }`. - Inner section constraints (timeline, gallery-grid, team-grid, panel, feature frames) MUST scale with the outer container — fixed caps like `max-width: 960px` on a timeline waste canvas on 1920px. - Mobile: `100vw - clamp(1rem, 4vw, 3rem)` so content uses 90%+ of small screens; never overflow on 360px phone. - Structured/interactive surfaces (timeline, dossier, stat-rollup, gallery, team grid, comparison table, partner strip, FAQ, content cards) USE FULL CONTAINER WIDTH; only reading prose stays ≤78ch. - Forbidden: hard pixel `max-width: 1120px` on shells when viewport is 2560px; hard `max-width: 640px` on cinematic prose blocks; `max-width: 960px` on timelines in widened containers; `width: 100%` on section while inner `<div class="content">` re-imposes narrower cap; static `padding-inline: 4rem` instead of `clamp(1rem, 4vw, 3rem)`; viewport units without `svh/dvh` fallback. - Every shell/panel/section uses fluid `min(<cinema-cap>, <viewport-pct>)`. - Validator (`validate-real-estate-usage.mjs`): Playwright at 6 breakpoints (375, 768, 1280, 1680, 1920, 2560) — fail if at 1920px+ widest content block uses <80% of viewport (excluding `[data-prose]` / `<article class="prose">` containers); fail if at 375px any block overflows (`scrollWidth > clientWidth`); fail if at 1280px canvas-utilization ratio across ALL non-prose sections averages <75%. Fail codes: `real-estate.dead-margin-ultrawide` | `real-estate.cramped-mobile` | `real-estate.fixed-pixel-shell` | `real-estate.inner-cap-overrides-container`. ## Every gallery image / carousel image (***LIGHTBOX MUST OPEN ON CLICK — UNIVERSAL — BUILD-BREAKING — extends "Every multi-image section"***) - Every `<img>` inside a `[data-gallery]` container, `[class*="carousel"]`, `[class*="slider"]`, `[class*="gallery"]`, `.swiper-slide`, `.splide__slide`, `.glide__slide`, or any multi-image grid section MUST open a fullscreen lightbox modal when clicked. - Lightbox MUST: (a) render within 300ms of click; (b) show full-resolution image (not thumbnail); (c) display caption from `data-caption-title` + `data-caption-description`; (d) support ←/→ arrows + Esc; (e) trap focus inside modal (WCAG 2.4.3); (f) restore scroll position on close; (g) announce `role="dialog" aria-modal="true" aria-label="Image lightbox"`. - Images NOT in gallery context (inline editorial photos, single hero, logo, icon, avatar <200px) MUST NOT open lightbox — pointer cursor honesty. - Lightbox eligibility: `kind != logo AND dims >= 200×200 AND inside multi-image parent`. - Template ships `src/lib/lightbox.ts` — MUST use BOTH `DOMContentLoaded` AND `MutationObserver` to wire all `[data-gallery] img` elements (React renders images ASYNCHRONOUSLY after DOMContentLoaded fires). ```js const wire = (img) => { if(!img.dataset.lightboxWired){ img.addEventListener('click',openLightbox); img.dataset.lightboxWired='1'; } }; document.querySelectorAll('[data-gallery] img').forEach(wire); new MutationObserver(muts => muts.flatMap(m=>[...m.addedNodes]).filter(n=>n.nodeType===1).forEach(n=>{ [...n.querySelectorAll('[data-gallery] img'), ...(n.matches('[data-gallery] img')?[n]:[])].forEach(wire); })).observe(document.body,{subtree:true,childList:true}) ``` - Every `<img>` inside `[data-gallery]` in static HTML output MUST have `data-zoomable data-caption-title="..." data-caption-description="..."` attributes — NOT added via JS after render. Lazy-loads full-res from `data-full-src` attr. - NEVER: click navigates to new page instead of lightbox; click opens empty/broken lightbox; click does nothing; only first image opens lightbox. - Validator (`validate-lightbox-opens-on-click.mjs`): Playwright on every route, finds ALL `[data-gallery] img`, clicks each, asserts `[role="dialog"]` appears within 500ms. `build_validators.ts validateLightboxPresence` checks ≥2 HTML img elements have `data-zoomable` attribute (not just JS bundle string). Fail codes: `lightbox.not_opened` | `lightbox.empty_modal` | `lightbox.html_wiring_incomplete`. ## Every site nav/header (***SOLID BACKGROUND — NEVER TRANSPARENT — UNIVERSAL — BUILD-BREAKING***) - Nav/header MUST have a solid, opaque background at ALL times — including homepage hero, hero-scroll overlap, mobile drawer open, any route. - Allowed exception: hero-parallax scroll effect where header transitions transparent → solid at ≤50px scroll (MUST be SOLID by 50px — never stays transparent). - Nav base CSS: `background-color: var(--nav-bg, var(--bg-primary)); backdrop-filter: blur(8px)` — blur adds depth but NEVER replaces a solid base. If hero uses full-bleed image behind nav area, nav MUST have an explicit solid band. - Build gate: Playwright screenshots nav at scrollY=0 on every route — samples computed `background-color` at nav centroid — any `rgba(* * * / 0)` or `transparent` = fail. Nav text/logo must contrast nav bg by ≥4.5:1 at 6bp. Sticky nav must maintain solid bg on scroll. ## Every site nav/header (***Z-INDEX STACK + CLIP PREVENTION — UNIVERSAL — BUILD-BREAKING***) - Nav MUST have `z-index: ≥100` and MUST NOT be clipped by hero sections, sliders (Swiper/Splide/Glide), or `overflow: hidden` parent wrappers. - If any hero/slider sets its own stacking context (`transform`, `will-change`, `filter`, `isolation`), the nav MUST live OUTSIDE that stacking context in the DOM. - Validator: at 6bp, Playwright samples nav `getBoundingClientRect().top` — assert nav always visible (top ≥ 0) AND nav element NOT contained inside any element with `overflow:hidden`. ## Every section (***VISUAL DISTINCTION — ALTERNATING TREATMENT — UNIVERSAL — BUILD-BREAKING***) - Adjacent sections MUST have visually distinct treatment so users can perceive section boundaries. - Allowed alternation strategies (pick ≥1 per boundary): (a) alternate bg `var(--bg-primary)` / `var(--bg-secondary)` / accent-tinted band; (b) geometric shape divider (wave, diagonal, angled clip-path); (c) `border-top 1px solid var(--border-subtle)`; (d) increased vertical padding creating visual breathing room. - NEVER two consecutive sections with identical `background-color` AND no divider AND touching vertical padding. - Validator: for each pair of sibling `<section>` elements, compute computed bg colors — if identical AND no `::before/::after` divider AND no border-top = fail. ## Every card grid (***UNIFORM IMAGE PRESENCE — UNIVERSAL — BUILD-BREAKING***) - In any card/tile grid, ALL cards MUST have images or NONE do — never mixed. - If ≥50% of items in corpus have images, ALL cards get images (augment missing ones via GPT Image 1.5 per-slot prompt). If <50% have images, render ALL cards without images (use icon/number/initial avatar as uniform accent). - Validator (`validate-card-image-uniformity.mjs`): for each `[data-card-grid]`, compute `cards_with_img / total_cards` — if `> 0` and `< 1.0` = fail. ## Every lightbox (***CSS BACKGROUND-IMAGE EXCLUDED — UNIVERSAL — BUILD-BREAKING — extends "Every gallery image"***) - CSS `background-image: url(...)` properties are NEVER lightbox-eligible — only `<img src>` and `<picture><source>` elements are eligible. - Lightbox auto-wiring script MUST query `[data-gallery] img` (HTMLImageElement only) — NEVER walk `getComputedStyle(el).backgroundImage`. - Validator: assert no `[data-gallery]` section has a lightbox click listener attached to a non-`<img>` DOM element. ## Every anchor-wrapped image (***LINK NAVIGATION WINS — UNIVERSAL — BUILD-BREAKING — extends "Every gallery image"***) - Any `<img>` whose nearest ancestor is `<a href="...">` MUST navigate to the link target on click — NEVER open a lightbox. - Lightbox handlers MUST early-return on `img.closest('a[href]')` and skip `cursor:zoom-in` marking. - Exception ONLY: anchor itself carries `data-lightbox` or `data-gallery` (explicit opt-in). Cursor: linked images use `cursor: pointer`, not `zoom-in`. ### Lightbox eligibility check order 1. Reject in header/footer/data-no-zoom/button 2. Accept if explicit `[data-lightbox]/[data-gallery]` opt-in 3. Reject if inside `a[href]` 4. Accept if dims ≥80×80 - Validator (`validate-anchor-image-no-lightbox.mjs`): Playwright finds every `a[href] img` on `/blog`, `/news`, `/portfolio`, listing pages — clicks each, asserts URL changes (navigation) AND no `[role="dialog"]` opens within 300ms. Fail code: `lightbox.intercepts_link_navigation`. ## Every photo/image (***GROUPING LABEL + SECTION SLUG — UNIVERSAL — BUILD-BREAKING — extends "Every multi-image section"***) - Every `data-gallery` attribute MUST have a companion `data-gallery-label` describing the photo set in 2-5 words. ```html data-gallery="team-photos" data-gallery-label="Leadership Team" ``` - The label renders in the lightbox modal header AND as `aria-label` on the `<figure>` wrapping element. - Every `img[data-zoomable]` in the section MUST share the section-slug as its `data-gallery` value. - Section-slug derivation: `kebabCase(section.dataset.section || section.id || h2/h3.textContent.trim().slice(0,30))`. - Validator: every `[data-gallery]` element in dist/ HTML must have a non-empty `data-gallery-label` attribute. ## Every image element (***DESCRIPTIVE ALT TEXT — UNIVERSAL — BUILD-BREAKING***) - Every `<img>` must have `alt` that: (a) is non-empty; (b) does NOT equal `"image"`, `"photo"`, `"picture"`, `"img"`, or the filename; (c) describes SUBJECT matter in 5-15 words. - Decorative images (pure CSS decoration conveying no information) use `alt=""` (intentional empty string). - Per-slot GPT Image 1.5 prompt must include `alt_text` in the JSON response schema — auto-applied to the generated img tag. - Validator: grep dist/ HTML for `alt=""` on non-decorative images (any img inside `[data-gallery]`, article, `section:not([data-decorative])`) = fail; grep for `alt="image"` or `alt="photo"` = fail. ## Every comparison table / data grid (***FULL-BLEED LAYOUT WHEN >1100px CONTENT — UNIVERSAL — BUILD-BREAKING — extends "Every full-width visual section"***) - Comparison tables (pricing tiers, feature matrices, product-spec tables) and data grids with ≥4 columns OR ≥1100px natural content width MUST render full-bleed AND MUST NOT use `overflow-x: scroll` to hide right columns on desktop. - Mobile (≤768px) reflow pattern: stack table as cards, each card showing one row with column headers as labels (`<dl>` per row OR CSS `display: contents` + grid with `grid-template-areas`). - Validator (`validate-comparison-table-fullbleed.mjs`): for every `<table data-comparison>` AND `[data-grid][data-cols≥4]`: at 1280px assert full-bleed (width ≥ container 100vw minus scrollbar) AND no right column clipped; at 375px assert table reflows to stacked cards (no horizontal scroll). ## Every page depth > 1 (***BREADCRUMBS — UNIVERSAL — BUILD-BREAKING***) - Every route with URL depth ≥2 (e.g. `/blog/post-slug`, `/team/jane-doe`, `/services/consulting`) MUST render a visible breadcrumb nav immediately below the page header / above the page H1 — never inside hero, never inside footer. ```html <nav aria-label="Breadcrumb"> <ol itemscope itemtype="https://schema.org/BreadcrumbList">…</ol> </nav> ``` Each breadcrumb item: ```html <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <a itemprop="item" href="/"><span itemprop="name">Home</span></a> <meta itemprop="position" content="1"/> </li> ``` - Final item: `<span itemprop="name">Current Page</span>` (NOT a link — current location is not clickable). - Visual separators via CSS `::after { content: "/" }` NOT embedded in HTML. - Breadcrumb JSON-LD `BreadcrumbList` on same page MUST match visible breadcrumb exactly (same items, order, names). - Validator (`validate-breadcrumbs.mjs`): for every route with URL depth ≥2, assert `nav[aria-label="Breadcrumb"]` present + `[itemtype*="BreadcrumbList"]` in head JSON-LD. ## Every site (***SKIP-TO-CONTENT FIRST FOCUSABLE — UNIVERSAL — BUILD-BREAKING — WCAG 2.4.1***) - The FIRST focusable element in every HTML page MUST be `<a href="#main-content" class="skip-link">Skip to main content</a>` (or locale-equivalent). ```css .skip-link { position: absolute; top: -60px; left: 0; z-index: 10000; background: var(--brand-accent); color: var(--bg-primary); padding: .5rem 1.5rem; font-weight: 700; border-radius: 0 0 4px 0; transition: top .15s ease } .skip-link:focus { top: 0 } ``` - Visible only on keyboard focus (Tab), invisible at rest. - `<main id="main-content" tabindex="-1">` MUST exist on every page; `tabindex="-1"` allows focus to land on main without showing outline. - Every SPA route change MUST re-move focus to h1 (or `<main>`) after navigation. - Validator (`validate-skip-link.mjs`): Playwright tabs to first focusable element, asserts `classList.contains("skip-link")` AND visible on focus AND `href` resolves to `#main-content` AND `#main-content` element exists. Fail codes: `a11y.skip_link_missing` | `a11y.skip_link_not_first` | `a11y.skip_target_missing`. ## Every site (***BACK-TO-TOP BUTTON — UNIVERSAL — BUILD-BREAKING***) - Every site with any page whose scrollable content exceeds two viewports MUST include: ```html <button class="back-to-top" aria-label="Back to top">↑</button> ``` - Rendered `position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 500`. - Hidden at `scrollY === 0`; appears with `opacity: 0→1; transform: translateY(8px)→translateY(0)` 200ms ease when `scrollY > 300`; click `window.scrollTo({ top: 0, behavior: 'smooth' })`. - `prefers-reduced-motion: reduce` — instant scroll, no appearance animation. - Min 44×44px hit target; min 3:1 contrast vs page bg. Bottom-right placement only. - Validator (`validate-back-to-top.mjs`): Playwright scrolls to `scrollY=1000`, asserts button visible + `aria-label="Back to top"` + click scrolls to 0 within 1s. ## Every accordion / collapsible / "Read More" (***SMOOTH HEIGHT + ARIA STATE — UNIVERSAL — BUILD-BREAKING — extends "Every expandable card"***) - **Collapsed** — `max-height: 0; overflow: hidden; opacity: 0`. - **Expanded** — JS reads `panel.scrollHeight`, sets `max-height: panel.scrollHeight + 'px'; opacity: 1` with `transition: max-height .3s cubic-bezier(.4,0,.2,1), opacity .2s ease`; after `transitionend` set `max-height: none`. - Trigger MUST have `aria-expanded="false"` (collapsed) / `aria-expanded="true"` (expanded) toggled on every click. Panel MUST have matching `aria-hidden="true"/"false"` + `role="region"` + `aria-labelledby="<trigger-id>"`. - Keyboard: Enter/Space — toggles; Escape — collapses and returns focus to trigger. - Animation disabled when `prefers-reduced-motion: reduce`. NEVER `display:none/block` toggle — use `aria-hidden`. - Validator (`validate-accordion-aria.mjs`): Playwright clicks every `[aria-expanded]` button, asserts panel height grows from 0 AND `aria-expanded` toggles AND `aria-hidden` toggles AND content visible after animation. ## Every modal dialog (***FOCUS TRAP + RESTORATION — UNIVERSAL — BUILD-BREAKING — WCAG 2.4.3***) - Every modal/dialog (`role="dialog"`, `role="alertdialog"`, lightbox, mobile menu overlay) MUST: (a) trap Tab cycle inside modal while open; (b) on open, move focus to first focusable OR `dialog.focus()`; (c) on close, return focus EXACTLY to the DOM element that triggered the open; (d) block background scroll via `document.body.style.cssText = "position:fixed;..."` pattern; (e) respond to Esc to close. - Native `<dialog>` element preferred (use `showModal()`). Polyfill with `a11y-dialog` for custom implementations. - Validator (`validate-modal-focus-trap.mjs`): Playwright opens each dialog/lightbox, presses Tab × 10, asserts focus stays within modal; closes via Esc, asserts focus returned to trigger. Fail codes: `a11y.focus_escapes_modal` | `a11y.focus_not_restored`. ## Every form (***ASSOCIATED LABELS + AUTOCOMPLETE + LOADING STATE — UNIVERSAL — BUILD-BREAKING***) - Every `<input>`, `<select>`, `<textarea>` MUST: (a) have explicit `<label for="input-id">` (NEVER placeholder-only — placeholder disappears on focus, fails WCAG 1.3.1); (b) have appropriate `autocomplete` attribute: `given-name`, `family-name`, `email`, `tel`, `organization`, `street-address`, `postal-code`, `cc-number` etc.; (c) have `aria-required="true"` when required. - Submit button MUST: show spinner + `aria-busy="true"` + `disabled` while in-flight (no double-submit); on success — replace form with success message telling user next step; on error — display inline field errors via `aria-describedby` linked to `<span role="alert">` per field, NOT just a generic toast. - Every form gets invisible Turnstile CAPTCHA (`data-appearance="interaction-only"`). - Validator (`validate-form-accessibility.mjs`): for every `<form>`, assert every `<input>:not([type="hidden"])` has `<label>` or `aria-label`, every field has `autocomplete` attr, submit button has loading-state implementation. ## Every `<table>` (***CAPTION + SCOPE HEADERS — UNIVERSAL — BUILD-BREAKING***) - Every `<table>` MUST have: (a) `<caption>` as first child (visible text, NOT `aria-label` on the table); (b) column headers as `<th scope="col">` in `<thead><tr>`; (c) row headers (when present) as `<th scope="row">` in each `<tbody><tr>`; (d) complex tables with multiple header rows MUST use `id`/`headers` linking. - Striped rows via `tbody tr:nth-child(even) { background: var(--row-alt) }`. Mobile reflow: stack as dl-cards via CSS `display: grid` with `data-label` attrs. - Validator (`validate-table-accessibility.mjs`): grep dist/ for `<table` — assert every table has `<caption>`, every `<th>` has `scope`, no `<td>` in `<thead>`. ## Every SVG icon (***ACCESSIBLE LABELING — UNIVERSAL — BUILD-BREAKING***) - Every `<svg>` conveying meaningful information MUST have: `role="img"` on the element; `<title id="icon-[name]-title">Descriptive text</title>` as first child; `aria-labelledby="icon-[name]-title"` on the element. - Every decorative `<svg>` MUST have `aria-hidden="true"` AND `focusable="false"` (IE11 compat). - Pattern: **Informative** — `<svg role="img" aria-labelledby="icon-email-title"><title id="icon-email-title">Send email</title>…</svg>`; **Decorative** — `<svg aria-hidden="true" focusable="false">…</svg>`. - SVGs inside `<button>` or `<a>` where parent already has accessible text: always use decorative pattern. - Validator (`validate-svg-accessibility.mjs`): for every `<svg>` NOT inside `<button>/<a>` with visible text — assert either `aria-hidden="true" focusable="false"` OR `role="img" aria-labelledby` present. ## Every site (***LATEST-TECH EXPERIENCE #1 — CONTAINER QUERIES OVER MEDIA QUERIES — UNIVERSAL — BUILD-BREAKING***) - Component-level responsive layouts MUST use CSS Container Queries (`container-type: inline-size; @container (min-width: 640px)`) — NOT viewport media queries — for any component that can appear in multiple layout slots. Media queries reserved ONLY for global page layout. ```css .card { container-type: inline-size; } @container (min-width: 400px) { .card-grid { grid-template-columns: 1fr 1fr; } } ``` - Validator (`validate-container-queries.mjs`): assert `@container` rule count ≥3 in `dist/**/*.css` AND any component CSS using width-based responsive logic uses `@container` not `@media (min-width:)`. Chrome 105+, Safari 16+, Firefox 110+. ## Every site (***LATEST-TECH EXPERIENCE #2 — :has() PARENT SELECTORS REPLACE JS CLASS-TOGGLING — UNIVERSAL — BUILD-BREAKING***) - MUST use `:has()` parent selector for state-dependent styling instead of JS class-toggling where CSS suffices. ```css form:has(input:invalid) .submit-btn { opacity: 0.5 } .card:has(img[data-loading="true"]) { background: skeleton-gradient } nav:has(.menu-open) .backdrop { display: block } ``` - Validator (`validate-has-selector.mjs`): assert ≥2 `:has(` occurrences in `dist/**/*.css` AND no JS `classList.add('is-invalid'|'is-loading'|'is-open')` patterns where `:has()` could replace them. Chrome 105+, Safari 15.4+, Firefox 121+. ## Every site (***LATEST-TECH EXPERIENCE #3 — @STARTING-STYLE FOR ENTER ANIMATIONS — UNIVERSAL — BUILD-BREAKING***) - Every component that enters the DOM (dialog opening, popover showing, list item adding, route content mounting) MUST use `@starting-style` for enter animation — NOT JS-orchestrated requestAnimationFrame double-rAF hack. ```css dialog { opacity: 1; @starting-style { opacity: 0; } } ``` - Pair with `transition-behavior: allow-discrete` for discrete property animations (display, visibility). - Validator (`validate-starting-style.mjs`): assert ≥2 `@starting-style` blocks in `dist/**/*.css` AND any modal/dialog/popover has paired enter transition. Chrome 117+, Safari 17.5+. ## Every site (***LATEST-TECH EXPERIENCE #4 — text-wrap: balance + pretty — UNIVERSAL — BUILD-BREAKING***) - MUST set `text-wrap: balance` on ALL headings (h1-h6) AND `text-wrap: pretty` on ALL body paragraphs (p, li, blockquote). ```css @layer base { h1,h2,h3,h4,h5,h6 { text-wrap: balance } p,li,blockquote { text-wrap: pretty } } ``` - Validator (`validate-text-wrap.mjs`): assert global `text-wrap: balance` targeting headings AND `text-wrap: pretty` targeting body text in `dist/**/*.css`. Chrome 114+ (balance), 117+ (pretty); Safari 17.5+. ## Every site (***LATEST-TECH EXPERIENCE #5 — OKLCH COLOR + COLOR-MIX — UNIVERSAL — BUILD-BREAKING***) - Color palette MUST use OKLCH (`oklch(70% 0.15 240)`) — NOT hex/RGB/HSL — for design tokens. Hover/active/disabled variants derived via `color-mix(in oklch, var(--accent) 80%, transparent)` — NOT hand-tuned hex pairs. Hex emitted only at compile time for legacy fallback via PostCSS plugin. - Validator (`validate-oklch-colors.mjs`): assert design-token CSS file uses `oklch(` ≥10 times AND ≥3 `color-mix(in oklch,` declarations for state variants. Chrome 111+, Safari 15.4+, Firefox 113+. ## Every site (***LATEST-TECH EXPERIENCE #6 — NATIVE CSS NESTING (no Sass) — UNIVERSAL — BUILD-BREAKING***) - Every component CSS MUST use native CSS nesting (`.card { & h2 { ... } &:hover { ... } &[data-state="loading"] { ... } }`) — NEVER ship Sass/Less/Stylus preprocessor. PostCSS only for `oklch→hex` legacy fallback + CSS minification. - Validator (`validate-native-nesting.mjs`): assert nested CSS blocks in `dist/**/*.css` parsed by browser-native CSS parser (no Sass `$var` or `@mixin` artifacts). Chrome 112+, Safari 16.5+, Firefox 117+. -
cinematic-doctrine.md 9.9 KB
# Cinematic Doctrine — Disney+/HBO/Showtime aesthetic (***ALWAYS — every premium build***) Every page reads like a streaming-platform splash, not a content blog. Marketing/non-profit/portfolio sites apply the FULL stack; SaaS dashboards apply EDGE-SAFE GUTTERS + FOCUS RINGS + LETTERBOX at minimum. ## Edge-safe gutters (***NON-NEGOTIABLE — never let a card kiss the viewport edge***) Cards touching the edge = AI-slop tell. Use a token, never a literal. ```css :root { --edge: max(1.75rem, env(safe-area-inset-left, 1.75rem)); --edge-tablet: max(1.5rem, env(safe-area-inset-left, 1.5rem)); --edge-mobile: max(1.25rem, env(safe-area-inset-left, 1.25rem)); --edge-tiny: max(1.125rem, env(safe-area-inset-left, 1.125rem)); } .container { padding-inline: var(--edge); } @media (max-width: 1024px) { .container { padding-inline: var(--edge-tablet); } } @media (max-width: 768px) { .container { padding-inline: var(--edge-mobile); } } @media (max-width: 480px) { .container { padding-inline: var(--edge-tiny); } } ``` `env(safe-area-inset-*)` handles notched/round-screen devices. Gate: `e2e/cinematic.spec.ts` asserts `.container` computed `padding-inline-start >= 16px` at all 6 breakpoints. ## Overflow containment (***BUILD-BREAKING — never ship a page with horizontal scroll***) ```css html { overflow-x: clip; } body { overflow-x: clip; } ``` - `clip` not `hidden` (no scroll container, cleaner) - Position-fixed pseudo-elements (`body::after { inset: -10% }`) DO contribute to `body.scrollWidth` in Chromium — always use `inset: 0` for fixed overlays, not negative insets - Animate via `transform: scale()` inside if you need bleed - Gate: `bodyScrollWidth <= viewportWidth + 1` at 6bp ## Cinematic layer (six fixed overlays — z-index orchestration) Lay them in this z-order: noise(0) → aurora(0) → vignette(1) → spotlight(1) → letterbox(2) → content(3+). Each must be `pointer-events:none` and `position:fixed` with `inset:0`. ### 1. Aurora bloom (establishing shot) Three drifting radial gradients, 32s alternate keyframe animation. `mix-blend-mode: normal`, opacity 0.95 → 1.0 → 0.90: ```css body::after { content:''; position:fixed; inset:0; pointer-events:none; z-index:0; background: radial-gradient(50% 40% at 18% 22%, rgba(80,170,227,.18), transparent 60%), radial-gradient(45% 38% at 82% 78%, rgba(0,229,255,.14), transparent 60%), radial-gradient(35% 30% at 60% 12%, rgba(124,58,237,.10), transparent 60%); animation: auroraDrift 32s ease-in-out infinite alternate; will-change: transform, opacity; } @keyframes auroraDrift { 0% { transform: translate3d(-2%,-1%,0) scale(1.00); opacity:.95; } 50% { transform: translate3d( 3%, 2%,0) scale(1.05); opacity:1.00; } 100% { transform: translate3d(-1%, 3%,0) scale(1.02); opacity:.90; } } ``` ### 2. Vignette (2.39:1 release-print darkening) Radial-darken corners + linear-darken top/bottom edges. `mix-blend-mode: multiply` deepens shadows without crushing midtones: ```css .vignette { position:fixed; inset:0; pointer-events:none; z-index:1; background: radial-gradient(ellipse at center, transparent 55%, rgba(6,6,16,.65) 100%), linear-gradient(180deg, rgba(6,6,16,.25) 0%, transparent 8%, transparent 92%, rgba(6,6,16,.35) 100%); mix-blend-mode: multiply; } ``` ### 3. Spotlight (pointer-driven follow light) Desktop only — `(pointer: fine)` media query gate. Updates `--mx/--my` via rAF-throttled mousemove handler. `mix-blend-mode: screen` makes it additive: ```css .spotlight { position:fixed; inset:0; pointer-events:none; z-index:1; opacity:0; background: radial-gradient(600px circle at var(--mx,50%) var(--my,50%), rgba(0,229,255,.06), transparent 60%); mix-blend-mode: screen; transition: opacity .4s ease; } @media (pointer: fine) { body.has-pointer .spotlight { opacity:1; } } ``` ```js (() => { let raf = 0, x = 50, y = 50; const s = document.getElementById('spotlight'); if (!s || !matchMedia('(pointer: fine)').matches) return; document.body.classList.add('has-pointer'); addEventListener('pointermove', (e) => { x = (e.clientX / innerWidth) * 100; y = (e.clientY / innerHeight) * 100; if (raf) return; raf = requestAnimationFrame(() => { s.style.setProperty('--mx', x+'%'); s.style.setProperty('--my', y+'%'); raf = 0; }); }, { passive:true }); })(); ``` ### 4. Letterbox bars (cinematic 2.39:1 framing on hero) 14px top + 28px bottom — top is shorter (cinema bars are intentionally asymmetric for visual weight): ```css .hero { position:relative; overflow:hidden; } .hero::before { content:''; position:absolute; inset:0 0 auto 0; height:14px; background:linear-gradient(180deg, rgba(6,6,16,1), transparent); z-index:2; } .hero::after { content:''; position:absolute; inset:auto 0 0 0; height:28px; background:linear-gradient( 0deg, rgba(6,6,16,1), transparent); z-index:2; } ``` ### 5. Color grading (body filter — entire page reads like a graded master) ```css body { filter: contrast(1.04) saturate(1.06); } ``` Don't go past 1.06/1.08 — destroys photo grades and pumps reds in skin tones. ### 6. Selection + focus (cinema-blue interactive states) ```css ::selection { background: rgba(0,229,255,.35); text-shadow: 0 0 12px rgba(0,229,255,.5); color: #fff; } :focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; box-shadow: 0 0 0 5px rgba(0,229,255,.15), 0 0 20px rgba(0,229,255,.4); border-radius: 8px; } ``` ## Random creative touches (***DELIGHT FLOOR — pick ≥3 per build***) Pre-built menu. Pick at least 3 — Brian's directive is "a lot of random creative stuff." 1. **Headline shimmer** — tri-stop gradient + `background-clip:text` + 8s slide animation on `h1, h2`: ```css .hero h1, section h2 { background: linear-gradient(90deg, #fff 0%, var(--cyan) 50%, #fff 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: titleShimmer 8s ease-in-out infinite; } @keyframes titleShimmer { 0%,100%{background-position:200% 0;} 50%{background-position:0 0;} } ``` 2. **Drop-cap on first manifesto paragraph** — `:first-of-type::first-letter`, 4rem cyan with text-shadow. 3. **3D card tilt** — perspective + custom-prop `rotateX/rotateY` from pointer position. Disabled on `(pointer: coarse)` and `prefers-reduced-motion`: ```css .card { transform: perspective(900px) rotateX(calc(var(--ty,0) * -3deg)) rotateY(calc(var(--tx,0) * 3deg)); transition: transform .3s ease; } @media (pointer: coarse), (prefers-reduced-motion: reduce) { .card { transform: none !important; } } ``` 4. **Cinema-poster gradient borders** — `mask-composite: exclude` to render only the border path: ```css .card::after { content:''; position:absolute; inset:0; border-radius:inherit; padding:1px; background: linear-gradient(135deg, var(--blue), var(--cyan), var(--purple)); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events:none; opacity:0; transition: opacity .4s ease; } .card:hover::after { opacity:1; } ``` 5. **Button shimmer-sweep on hover** — `::before` linear-gradient sliding `left: -75% → 125%`: ```css .hero-cta { position:relative; overflow:hidden; } .hero-cta::before { content:''; position:absolute; top:0; left:-75%; width:50%; height:100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,.2), transparent); transform: skewX(-25deg); transition: left .6s ease; } .hero-cta:hover::before { left: 125%; } ``` 6. **Establishing-shot section glow** — `section:not(.hero)::before` radial gradient overhead, varies per section. 7. **Decorative chapter glyph** — `.section-header::after` 80px gradient line + diamond/dot, like an HBO scene-divider. 8. **Card-image bottom-fade** — `.card-img-wrap::after` linear-gradient overlay (0deg, black, transparent). 9. **Anti-FOUC + in-viewport fadeIn** — `body { animation: pageFadeIn .5s ease-out; }` + IntersectionObserver `.fade-in-section`. 10. **Page transitions via View Transitions API** — `@view-transition { navigation: auto; }` + cross-fade. 11. **Konami code easter egg** — secret animation on `↑↑↓↓←→←→BA` (keep <1KB). 12. **Branded scrollbar** — `scrollbar-color: var(--blue) var(--black);` + webkit overrides 8px wide. 13. **Scroll-driven hero parallax** — `@supports (animation-timeline: scroll())` only, fallback `transform: none`. 14. **Custom selection cursor** — `cursor: url('data:image/svg+xml,...'), auto;` near interactive elements. 15. **Footer signature animation** — copyright fades in last, with cyan period blink. ## Reduced-motion override (***ACCESSIBILITY — every cinematic element***) ```css @media (prefers-reduced-motion: reduce) { body::after { animation: none; } .hero h1, section h2 { animation: none; -webkit-text-fill-color: currentColor; background: none; } .card, .help-card { transform: none !important; transition: none; } .hero-cta::before { display: none; } body { filter: none; } } ``` ## Mobile downgrade - **Spotlight** — hidden via `@media (pointer: coarse)` - **3D tilt** — disabled (`transform: none`) - **Letterbox** — reduce to 8px / 16px below 768px - **Aurora** — opacity 0.6 below 480px (battery + contrast) ## Build gate (***E2E SPEC — `e2e/cinematic.spec.ts` MANDATORY***) Six-breakpoint guard. Per breakpoint asserts: - (a) `bodyScrollWidth ≤ viewportWidth + 1` (no horizontal scroll) - (b) `htmlScrollWidth ≤ viewportWidth + 1` - (c) `.container` computed `padding-inline-{start,end} ≥ 16px` - (d) `.vignette` + `#spotlight` attached - (e) `getComputedStyle(body).filter` matches `/contrast|saturate/` Failure = build broken. ## File-map (drop-in CSS bundle path) Store the canonical bundle at `~/.agentskills/10-experience-and-design-system/cinematic-bundle.css` (next iteration). Include all six layers + delight menu items + reduced-motion overrides. Site-gen skill 15 imports + tokens-swaps colors for brand fit. -
design-tokens.md 7.6 KB
--- name: "Design Tokens" version: "1.2.0" updated: "2026-04-23" description: "Emdash brand tokens (#060610/#00E5FF/#50AAE3/#7C3AED). HSL scales 50-950, 4px spacing, 1.25 type scale, W3C DTCG JSON format. CSS custom properties + TypeScript constants. Dark-first with light overrides." --- # Design Tokens ## Color Scale Generation (HSL manipulation) ```typescript // scripts/generate-tokens.ts function hexToHsl(hex: string): [number, number, number] { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); const l = (max + min) / 2; if (max === min) return [0, 0, l * 100]; const d = max - min; const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); let h = 0; if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; else if (max === g) h = ((b - r) / d + 2) / 6; else h = ((r - g) / d + 4) / 6; return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)]; } function generateScale(hex: string): Record<string, string> { const [h, s] = hexToHsl(hex); // Tailwind-style 50-950 scale: lightest → darkest const lightnesses: Record<string, number> = { '50': 97, '100': 94, '200': 86, '300': 76, '400': 64, '500': 50, '600': 40, '700': 32, '800': 24, '900': 17, '950': 10, }; const scale: Record<string, string> = {}; for (const [step, l] of Object.entries(lightnesses)) { scale[step] = `hsl(${h}, ${s}%, ${l}%)`; } return scale; } ``` ## Token File: CSS Custom Properties ```css /* src/styles/tokens.css */ /* === Color Tokens (Dark-First) === */ :root { /* Brand primaries */ --color-bg: #060610; --color-bg-secondary: #0a0a1a; --color-bg-tertiary: #121225; --color-bg-card: #0f0f1f; --color-bg-elevated: #1a1a35; /* Cyan scale (#00E5FF) */ --color-cyan-50: hsl(187, 100%, 97%); --color-cyan-100: hsl(187, 100%, 94%); --color-cyan-200: hsl(187, 100%, 86%); --color-cyan-300: hsl(187, 100%, 76%); --color-cyan-400: hsl(187, 100%, 64%); --color-cyan-500: #00E5FF; --color-cyan-600: hsl(187, 100%, 40%); --color-cyan-700: hsl(187, 100%, 32%); --color-cyan-800: hsl(187, 100%, 24%); --color-cyan-900: hsl(187, 100%, 17%); --color-cyan-950: hsl(187, 100%, 10%); /* Blue scale (#50AAE3) */ --color-blue-50: hsl(207, 70%, 97%); --color-blue-100: hsl(207, 70%, 94%); --color-blue-200: hsl(207, 70%, 86%); --color-blue-300: hsl(207, 70%, 76%); --color-blue-400: hsl(207, 70%, 64%); --color-blue-500: #50AAE3; --color-blue-600: hsl(207, 70%, 40%); --color-blue-700: hsl(207, 70%, 32%); --color-blue-800: hsl(207, 70%, 24%); --color-blue-900: hsl(207, 70%, 17%); --color-blue-950: hsl(207, 70%, 10%); /* Purple scale (#7C3AED) */ --color-purple-50: hsl(263, 83%, 97%); --color-purple-100: hsl(263, 83%, 94%); --color-purple-200: hsl(263, 83%, 86%); --color-purple-300: hsl(263, 83%, 76%); --color-purple-400: hsl(263, 83%, 64%); --color-purple-500: #7C3AED; --color-purple-50: hsl(263, 83%, 97%); --color-purple-600: hsl(263, 83%, 40%); --color-purple-700: hsl(263, 83%, 32%); --color-purple-800: hsl(263, 83%, 24%); --color-purple-900: hsl(263, 83%, 17%); --color-purple-950: hsl(263, 83%, 10%); /* Text */ --color-text: #f0f0f5; --color-text-secondary: #a0a0b5; --color-text-muted: #606080; /* Borders */ --color-border: rgba(255, 255, 255, 0.06); --color-border-hover: rgba(255, 255, 255, 0.12); /* Semantic */ --color-action: var(--color-cyan-500); --color-action-hover: var(--color-cyan-400); --color-success: #22c55e; --color-warning: #eab308; --color-error: #ef4444; /* === Spacing (4px base) === */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-6: 24px; --space-8: 32px; --space-12: 48px; --space-16: 64px; --space-24: 96px; --space-32: 128px; /* === Typography (modular scale 1.25 — minor third) === */ --text-xs: 0.75rem; /* 12px */ --text-sm: 0.875rem; /* 14px */ --text-base: 1rem; /* 16px */ --text-lg: 1.25rem; /* 20px */ --text-xl: 1.563rem; /* 25px */ --text-2xl: 1.953rem; /* 31.25px */ --text-3xl: 2.441rem; /* 39px */ --text-4xl: 3.052rem; /* 48.8px */ --text-hero: 3.815rem; /* 61px */ --leading-tight: 1.1; --leading-snug: 1.25; --leading-normal: 1.6; --leading-relaxed: 1.75; --tracking-tight: -0.02em; --tracking-normal: 0; --tracking-wide: 0.04em; --tracking-widest: 0.1em; /* === Shadows === */ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.35); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.6); --shadow-glow-cyan: 0 0 20px rgba(0, 229, 255, 0.15); --shadow-glow-purple: 0 0 20px rgba(124, 58, 237, 0.15); /* === Border Radius === */ --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-full: 9999px; /* === Transitions === */ --duration-fast: 100ms; --duration-normal: 200ms; --duration-slow: 300ms; --ease-default: cubic-bezier(0.4, 0, 0.2, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-out: cubic-bezier(0, 0, 0.2, 1); } /* === Light Theme Overrides === */ [data-theme="light"] { --color-bg: #f8f9fa; --color-bg-secondary: #f0f1f3; --color-bg-tertiary: #e8e9eb; --color-bg-card: #ffffff; --color-bg-elevated: #ffffff; --color-text: #1a1a2e; --color-text-secondary: #4a4a6a; --color-text-muted: #8a8aa0; --color-border: rgba(0, 0, 0, 0.08); --color-border-hover: rgba(0, 0, 0, 0.16); --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12); --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.15); } ``` ## Token File: TypeScript Constants ```typescript // src/styles/tokens.ts export const colors = { bg: '#060610', bgSecondary: '#0a0a1a', bgTertiary: '#121225', bgCard: '#0f0f1f', bgElevated: '#1a1a35', cyan: { 50: 'hsl(187,100%,97%)', 500: '#00E5FF', 900: 'hsl(187,100%,17%)' }, blue: { 50: 'hsl(207,70%,97%)', 500: '#50AAE3', 900: 'hsl(207,70%,17%)' }, purple: { 50: 'hsl(263,83%,97%)', 500: '#7C3AED', 900: 'hsl(263,83%,17%)' }, text: '#f0f0f5', textSecondary: '#a0a0b5', textMuted: '#606080', action: '#00E5FF', success: '#22c55e', warning: '#eab308', error: '#ef4444', } as const; export const spacing = { 1: '4px', 2: '8px', 3: '12px', 4: '16px', 6: '24px', 8: '32px', 12: '48px', 16: '64px', 24: '96px', 32: '128px', } as const; export const fontSize = { xs: '0.75rem', sm: '0.875rem', base: '1rem', lg: '1.25rem', xl: '1.563rem', '2xl': '1.953rem', '3xl': '2.441rem', '4xl': '3.052rem', hero: '3.815rem', } as const; export const radius = { sm: '4px', md: '8px', lg: '12px', full: '9999px' } as const; export type ColorToken = keyof typeof colors; export type SpacingToken = keyof typeof spacing; export type FontSizeToken = keyof typeof fontSize; export type RadiusToken = keyof typeof radius; ``` ## Usage ```css /* Apply tokens */ .card { background: var(--color-bg-card); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-6); box-shadow: var(--shadow-md); transition: border-color var(--duration-normal) var(--ease-default), box-shadow var(--duration-normal) var(--ease-default); } .card:hover { border-color: var(--color-border-hover); box-shadow: var(--shadow-glow-cyan); } ``` ## Conventions - **Dark-first** — all tokens defined for dark; light is the override, not the default. - `prefers-color-scheme: light` maps to `[data-theme="light"]`. - **Never hardcode colors** — always reference tokens. -
SKILL.md 7.9 KB
--- name: "experience-and-design-system" description: "Anti-AI-slop design system for distinctive, premium interfaces. Bold typography, dark-first #060610, fluid clamp() type, cascade layers + native nesting + container queries, OKLCH color, @starting-style, View Transitions API, DTCG tokens." metadata: version: "2.1.0" updated: "2026-05-03" effort: "high" model: "opus" license: "Rutgers" compatibility: claude-code: ">=2.0.0" agentskills: ">=1.0.0" submodules: - build-breaking-rules.md - cinematic-doctrine.md - design-tokens.md priority: 3 pack: "design" stage: stable triggers: - "design" - "ui" - "components" - "theme" paths: - "org:website_build" --- # 10 — Experience and Design System Build anti-AI-slop premium interfaces: dark-first OKLCH color, fluid `clamp()` type, DTCG tokens, View Transitions on every public-facing surface. **Apple Test** — after every design: two elements compete → remove one; crowded → add whitespace; busy type → reduce sizes, increase weight contrast; final feel: effortless, inevitable. ## CSS Patterns - Overlay `rgba(0,0,0,0.81)` · Text shadow `1px 1px 1px rgba(255,255,255,0.333)` · Box shadow `2px 2px 2px rgba(0,0,0,0.69)` - Border-radius 5px interactive, 10px containers (never 0, never pill) - Hero padding 40px · Max text 720px · Line-height 1.4 - Letter-spacing: 0.4px labels, 0.5px nav, 1px titles, 1.4px CTAs - CTA uppercase always · Button 700 always · Reference: Linear, Notion, Stripe ## Typography - Body: Sora 400/500 · Headings: Space Grotesk 600/700 · Mono: JetBrains Mono 400/500 · Display: Clash Display 700 (hero only) - Variable fonts: WOFF2, subset, self-host (never Google Fonts CDN), `font-display:swap` - Body min 16px (prefer 18) · Line-height 1.6 body, 1.1-1.2 headings · Letter-spacing -0.02em >2rem · Max 65ch - Never skip levels. Scale: Minor Third 1.2 general, Perfect Fourth 1.333 marketing - `text-wrap:balance` headings, `text-wrap:pretty` paragraphs ```css :root { --text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem); --text-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem); --text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); --text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.25rem); --text-xl: clamp(1.25rem, 1rem + 1.25vw, 1.5rem); --text-2xl: clamp(1.5rem, 1.1rem + 2vw, 2rem); --text-3xl: clamp(2rem, 1.5rem + 2.5vw, 3rem); --text-4xl: clamp(2.5rem, 1.5rem + 5vw, 4.5rem); --text-hero: clamp(3rem, 2rem + 5vw, 6rem); } ``` ## Color (Dark Default) ```css :root { color-scheme: light dark; --bg-primary: #060610; --bg-secondary: #0a0a1a; --bg-tertiary: #121225; --bg-card: #0f0f1f; --bg-elevated: #1a1a35; --text-primary: #f0f0f5; --text-secondary: #a0a0b5; --text-muted: #606080; --accent-cyan: #00E5FF; --accent-blue: #50AAE3; --accent-purple: #8B5CF6; --gradient-primary: linear-gradient(135deg, #00E5FF, #50AAE3); --gradient-accent: linear-gradient(135deg, #50AAE3, #8B5CF6); --border-subtle: rgba(255,255,255,0.06); --border-hover: rgba(255,255,255,0.12); --shadow-sm: 0 1px 2px rgba(0,0,0,0.3); --shadow-md: 0 4px 12px rgba(0,0,0,0.4); --shadow-lg: 0 8px 32px rgba(0,0,0,0.5); --shadow-glow: 0 0 20px rgba(0,229,255,0.15); } ``` - Never `#000` (use `#060610`) · Never `#fff` (use `#f0f0f5`) - Cyan: primary CTAs · Blue: secondary · Gradients on buttons only · 6% borders · Subtle glow on primary interactive - Elevation via lightness not shadows: base → surface 1 → surface 2 → surface 3 - `color-scheme:light dark` · `data-theme="dark|light"` user override · `localStorage` persistence · `prefers-color-scheme` system default · Always provide toggle - OKLCH perceptually uniform · `color-mix()` · Relative `oklch(from var(--brand) l c calc(h + 30))` · `light-dark()` theme-aware - Contrast 4.5:1 normal, 3:1 large/UI (WCAG 2.2 AA) · Target size min 24×24 px (2.5.8) · Focus 2px thick, 3:1 contrast (2.4.13) ## CSS Architecture (2026) ```css @layer reset, base, tokens, components, utilities, overrides; ``` - Native nesting · Container queries (`container-type:inline-size`, `@container`) · `:has()` replaces JS - `@scope` bounded styling · Anchor positioning replaces Floating UI · Scroll-state queries `@container scroll-state(stuck: top)` (Baseline 2026) - CSS `if()` · Typed `attr()` · `sibling-index()` / `sibling-count()` stagger: `transition-delay: calc((sibling-index() - 1) * 40ms)` - `appearance:base-select` native `<select>` (Chrome 135+) · `@supports` for progressive enhancement - **Baseline 2026**: `@scope` · Anchor positioning · Scroll-state queries · `@starting-style` · `interpolate-size: allow-keywords` · `field-sizing: content` · `text-wrap: pretty` · `@property` ## W3C DTCG Design Tokens (2025.10 Stable) - JSON `.tokens` / `.tokens.json`, MIME `application/design-tokens+json` - Token: `$value` (required), `$type`, `$description`, `$deprecated`, `$extensions` - Types: color | dimension | duration | fontFamily | fontWeight | cubicBezier | number + composites (shadow, border, gradient, typography, transition) - Aliasing: `"$value": "{base.color}"` · `$ref` JSON Pointer · Group `$type` inheritance · `$root` for base + variants · `$extends` for deep merge - Full Display P3, OKLCH, CSS Color Module 4; naming: no `$` prefix, no `{}` / `.` in names - Tools: Tokens Studio, Style Dictionary, Penpot, Figma ## AI-Ready Design Documentation - `DESIGN.md`: plain-text markdown; sections: Visual Theme, Color Palette, Typography, Spacing + Layout, Components, Elevation - Atomic docs: context-rich units per component with states, variants, props, constraints, a11y, rationale - MCP servers (Figma Dev Mode MCP) for programmatic access ## Layout - Container 1140px (wide 1400, narrow 720), padding `clamp(1rem,3vw,3rem)` - Sections `clamp(4rem,8vw,8rem)`, border between; grid `auto-fit minmax(280px,1fr)`, 1fr at 768px - Patterns: Hero (full-viewport, centered) · Features (3-col icon+heading+desc) · Alternating (zigzag) · Pricing (3-tier highlighted) · FAQ (accordion) · CTA (full-width dark) · Footer (4-col stack) - SaaS: Single-CTA 13.5%; Hero / Proof / Features / Demo / Testimonials / Pricing / FAQ / CTA; bento grid for feature showcases ## Components - **Cards** — bg-card, border-subtle, 12px radius, hover: border-hover + shadow-glow - **Buttons** — Primary gradient #060610 text, 600 weight, 8px radius, hover 0.9, active scale(0.98), focus 3px cyan. Secondary: transparent, border, hover cyan - **Nav** — sticky, `rgba(6,6,16,0.85)`, `blur(16px)` - **Forms** — bg-secondary, border-subtle, 8px, focus cyan + glow - **PrimeNG** — standalone (not NgModule), OnPush on all, lazy-load heavy (DataTable, Editor, Chart), design tokens for theming ## Interaction (every interactive element) `cursor:pointer`, hover state, focus-visible (3px cyan, 2px offset), active (scale 0.98), transition (0.2s color, 0.1s transform). WCAG 2.2 — min 24×24px targets, focus not obscured by sticky headers, dragging alternatives required, accessible auth. ### 4-state distinction (NON-NEGOTIABLE) Every link/button/card MUST visually differ across `:default | :hover | :focus-visible | :active` — NEVER let two states look identical. - Default → neutral · Hover → underline-sweep + color shift + `translateY(-1px)` · Focus-visible → 3px cyan ring 2px offset (distinct from hover) · Active → `scale(0.98)` + immediate color confirm Audit gate: Playwright cycles each interactive element through 4 states → screenshots → diff ≥3px pixel-difference between adjacent states or fail. ### Underline-sweep (text links default) ```css .underline-hover{position:relative} .underline-hover::after{content:"";position:absolute;z-index:1;left:51%;right:51%;bottom:0;background:var(--brand-accent);height:1px;transition:left .3s ease-out,right .3s ease-out} .underline-hover:hover::after,.underline-hover:focus-visible::after{left:0;right:0} ``` `var(--brand-accent)` / theme equivalent — never hard-coded #hex. ## See submodules: design-tokens.md, build-breaking-rules.md, cinematic-doctrine.md.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.