Claude Cursor GitHub Copilot opencode Skill

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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download heymegabyte-claude-skills-10-experience-and-design-system-e7acb91.zip · 25 KB
Part of heymegabyte/claude-skills — 18 skills

Install

skills CLI npx skills add https://github.com/heymegabyte/claude-skills/tree/master/10-experience-and-design-system
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
Git 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 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
: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 · 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)

@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)

.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.

No comments yet.

Reviews (0)

No reviews yet.

Related