#!/usr/bin/env python3
"""Build the public design-system catalog under docs/design-systems/.

For each named system: synthesize a design language (the engine guarantees WCAG
AA), emit the full folder via system_pack (DESIGN.md + css/tokens.css +
metadata.json + preview.html), zip it for one-click download, and collect a row
for index.json that the /design-systems gallery renders from.

Every system here is generated by the engine. Names and concepts are ours; the
tokens are synthesized, not hand-picked. Re-run to regenerate deterministically:

    python3 scripts/build_design_systems.py
"""
from __future__ import annotations

import json
import shutil
import sys
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
from typing import List

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from engine.synthesizer import synthesize
from engine.generator.system_pack import pack_system

OUT = Path("docs/design-systems")


@dataclass
class _Brief:
    industry: str = ""
    tone: List[str] = field(default_factory=list)
    audience: List[str] = field(default_factory=list)
    must_have: List[str] = field(default_factory=list)
    forbidden: List[str] = field(default_factory=list)
    reference_brands: List[str] = field(default_factory=list)
    strict: bool = False


# (slug, name, brief, description, concept, tags)
CATALOG = [
    ("pulsar", "Pulsar",
     _Brief(industry="crypto", tone=["bold"]),
     "A deep-space dark system where an electric magenta carries every action.",
     "Pulsar treats the screen as a near-black void and lets a single electric magenta do the "
     "signalling. Hairline borders draw the structure, mono type handles the numbers, and the canvas "
     "stays out of the way so data reads as light. Built for crypto, trading, and AI surfaces.",
     ["Dark", "Vivid", "Data", "Accessible"]),
    ("redline", "Redline",
     _Brief(industry="gaming", tone=["loud", "kinetic"]),
     "A high-energy near-black system with a crimson edge.",
     "Redline runs hot. A crimson accent cuts across a true near-black field, type stays tight and "
     "confident, and motion is quick. Made for gaming, launches, and any interface that should feel "
     "kinetic without losing its footing.",
     ["Dark", "Bold", "Kinetic", "Accessible"]),
    ("almanac", "Almanac",
     _Brief(industry="editorial-media", tone=["warm", "editorial"]),
     "A warm editorial system on cream paper with a tall display face.",
     "Almanac reads like print. Warm cream paper, a tall display face, generous line height, and a "
     "single burnt-orange rule for emphasis. For long-form reading, editorial sites, and brands that "
     "value the written word.",
     ["Light", "Editorial", "Warm", "Accessible"]),
    ("bordeaux", "Bordeaux",
     _Brief(industry="luxury", tone=["refined", "minimal"]),
     "A restrained luxury system: deep wine on warm cream.",
     "Bordeaux is unhurried. A deep wine accent sits on warm cream, the type is quiet and well-set, "
     "and nothing shouts. For luxury, hospitality, and considered brands that earn attention rather "
     "than demand it.",
     ["Light", "Luxury", "Minimal", "Accessible"]),
    ("marigold", "Marigold",
     _Brief(industry="ecommerce", tone=["playful", "warm"]),
     "A sunlit system: pale gold canvas, warm rust accent.",
     "Marigold is cheerful and tactile. A sunlit pale-gold canvas, a warm rust accent, and rounded, "
     "friendly type. For commerce, consumer apps, and brands that want to feel approachable and warm.",
     ["Light", "Playful", "Warm", "Accessible"]),
    ("quartz", "Quartz",
     _Brief(industry="saas", tone=["clean", "trustworthy"]),
     "A crisp, trustworthy SaaS system in cool white.",
     "Quartz is clean and calm. A crisp cool-white surface, a confident accent, clear hierarchy, and "
     "no decoration for its own sake. For SaaS, dashboards, and product UI where clarity is the brand.",
     ["Light", "Clean", "SaaS", "Accessible"]),
    ("ledger", "Ledger",
     _Brief(industry="fintech-payments", tone=["bold", "precise"]),
     "A precise fintech system with tight type and a decisive accent.",
     "Ledger is exact. A cool-white surface, tight typography, dense-friendly spacing, and a decisive "
     "accent for the one action that matters. For fintech, analytics, and dashboards full of numbers.",
     ["Light", "Fintech", "Precise", "Accessible"]),
    ("cedar", "Cedar",
     _Brief(reference_brands=["spotify"]),
     "A grounded system with a forest-green accent on warm white.",
     "Cedar feels natural. A grounded forest-green accent on warm white, calm spacing, and honest "
     "type. For media, wellness, and lifestyle brands that want warmth without noise.",
     ["Light", "Natural", "Calm", "Accessible"]),
    ("iris", "Iris",
     _Brief(reference_brands=["stripe"]),
     "A composed system: deep indigo on warm white.",
     "Iris is composed and credible. A deep indigo accent on warm white, even rhythm, and type that "
     "stays out of the way. For fintech, payments, and products that need to feel trustworthy at a glance.",
     ["Light", "Composed", "Fintech", "Accessible"]),
]


def _zip_folder(folder: Path, zip_path: Path) -> None:
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
        for f in sorted(folder.rglob("*")):
            if f.is_file():
                z.write(f, f.relative_to(folder.parent))


_CHROME = None


def _chrome():
    """Extract (styles, font-links, body-open+nav, footer+scripts) from the site
    chrome once, so every generated page stays visually consistent with the site."""
    global _CHROME
    if _CHROME is None:
        import re
        brands = Path("docs/brands.html").read_text(encoding="utf-8")
        head = brands[:brands.index("</head>")]
        styles = "\n".join(re.findall(r"<style[^>]*>.*?</style>", head, re.DOTALL))
        # Drop brands.html page-specific rules (.br-*): dead weight on our pages,
        # plus a stray `.br-search:focus{outline:none}` the linter flags critical.
        # The shared chrome (tokens, usknav, footer) has no .br- selectors.
        styles = re.sub(r'[^{}]*\.br-[^{}]*\{[^}]*\}', '', styles)
        _CHROME = (
            styles,
            "\n".join(re.findall(r'<link[^>]+(?:fonts\.googleapis|fonts\.gstatic|preconnect)[^>]*>', head)),
            brands[brands.index("</head>") + len("</head>"):brands.index('<section class="top">')],
            brands[brands.index("<footer"):],
        )
    return _CHROME


_GALLERY_CSS = """<style id="ds-gallery">
.ds-wrap{max-width:1120px;margin:0 auto;padding:0 24px}
.ds-hero{padding:72px 0 28px}
.ds-eyebrow{font:600 12px/1 ui-monospace,SFMono-Regular,monospace;letter-spacing:.16em;text-transform:uppercase;color:var(--accent,#e8c33a);margin:0 0 18px}
.ds-hero h1{font-size:clamp(34px,5.4vw,56px);line-height:1.04;letter-spacing:-.02em;margin:0 0 18px;max-width:16ch}
.ds-lede{font-size:18px;line-height:1.6;max-width:60ch;opacity:.82;margin:0 0 24px}
.ds-cta-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:18px}
.ds-cta{display:inline-flex;align-items:center;gap:8px;font-weight:600;font-size:15px;padding:12px 22px;border-radius:10px;text-decoration:none;background:var(--accent,#e8c33a);color:#0a0b0d}
.ds-cta--ghost{background:transparent;color:inherit;border:1px solid rgba(255,255,255,.18)}
.ds-pitch{font-size:14px;line-height:1.6;opacity:.6;max-width:64ch;margin:0}
.ds-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(304px,1fr));gap:22px;padding:30px 0 8px}
.dsx{border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.1);display:flex;flex-direction:column;min-width:0}
.dsx__live{background:var(--c);padding:24px 22px 20px;flex:1}
.dsx__eyebrow{font:600 11px/1 ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase;color:var(--p)}
.dsx__name{font-size:27px;font-weight:700;letter-spacing:-.02em;color:var(--i);margin:11px 0 6px}
.dsx__desc{font-size:13.5px;line-height:1.5;color:var(--b);margin:0 0 15px}
.dsx__sw{display:flex;gap:5px;margin-bottom:16px}
.dsx__sw span{width:23px;height:23px;border-radius:6px;border:1px solid rgba(0,0,0,.08)}
.dsx__demo{display:flex;gap:9px;align-items:center}
.dsx__btn{background:var(--p);color:var(--op);font:600 12.5px system-ui,sans-serif;padding:8px 15px;border-radius:8px}
.dsx__chip{border:1px solid var(--p);color:var(--p);font:600 12px system-ui,sans-serif;padding:6px 12px;border-radius:999px}
.dsx__foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px 12px 16px;background:rgba(10,11,14,.6);border-top:1px solid rgba(255,255,255,.08)}
.dsx__aa{display:inline-flex;align-items:center;gap:6px;font:600 11px ui-monospace,monospace;color:#86c79b}
.dsx__acts{display:flex;gap:13px}
.dsx__acts a{font:600 12px system-ui,sans-serif;color:#c2c6d0;text-decoration:none;white-space:nowrap}
.dsx__acts a:hover{color:#fff}
.dsx__acts a.dl{color:#fff}
.dsx{position:relative}
.dsx__name a{color:inherit;text-decoration:none}
.dsx__name a::after{content:"";position:absolute;inset:0;z-index:1}
.dsx__acts a{position:relative;z-index:2}
.ds-how{padding:40px 0 80px;border-top:1px solid rgba(255,255,255,.08);margin-top:40px}
.ds-how h2{font-size:24px;letter-spacing:-.01em;margin:0 0 12px}
.ds-how p{font-size:15px;line-height:1.6;opacity:.78;max-width:62ch;margin:0 0 16px}
.ds-code{display:block;font:500 13.5px ui-monospace,monospace;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:14px 16px;overflow-x:auto;color:#e8e9ee;margin:0 0 16px}
.ds-how a{color:var(--accent,#e8c33a)}
@media (max-width:560px){.ds-grid{grid-template-columns:1fr}.ds-hero{padding:48px 0 20px}}
.usknav__drawer{display:none}
.usknav__drawer.is-open{display:flex;flex-direction:column}
</style>"""


def _swatch_spans(sw):
    return "".join(f'<span style="background:{c}"></span>' for c in sw)


def _card(s):
    p = s["palette"]
    return (
        f'<article class="dsx" style="--c:{p["canvas"]};--i:{p["ink"]};--b:{p["body"]};'
        f'--p:{p["primary"]};--op:{p["on_primary"]}">'
        f'<div class="dsx__live">'
        f'<div class="dsx__eyebrow">{s["display_font"]} &middot; {s["theme"]}</div>'
        f'<h3 class="dsx__name"><a href="/design-systems/{s["slug"]}/">{s["name"]}</a></h3>'
        f'<p class="dsx__desc">{s["description"]}</p>'
        f'<div class="dsx__sw">{_swatch_spans(s["swatches"])}</div>'
        f'<div class="dsx__demo"><span class="dsx__btn">Primary</span>'
        f'<span class="dsx__chip">accent</span></div>'
        f'</div>'
        f'<div class="dsx__foot">'
        f'<span class="dsx__aa"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" '
        f'stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" '
        f'aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>AA {s["contrast"]["ink_on_canvas"]}:1</span>'
        f'<span class="dsx__acts">'
        f'<a href="/design-systems/{s["slug"]}/">View system</a>'
        f'<a class="dl" href="/design-systems/{s["slug"]}.zip" download>Download</a>'
        f'</span></div></article>'
    )


def build_gallery(systems):
    styles, fonts, nav, tail = _chrome()

    n = len(systems)
    jsonld = json.dumps({
        "@context": "https://schema.org", "@type": "CollectionPage",
        "name": "Free design systems compiled by ux-skill",
        "description": f"{n} free, accessible design systems generated by the ux-skill engine. "
                       "Each ships as a full folder (DESIGN.md, tokens, metadata, preview) and passes WCAG AA.",
        "url": "https://uxskill.laithjunaidy.com/design-systems.html",
        "hasPart": [{"@type": "CreativeWork", "name": s["name"], "abstract": s["description"],
                     "url": f"https://uxskill.laithjunaidy.com/design-systems/{s['slug']}/"} for s in systems],
    }, ensure_ascii=False)

    head_out = (
        '<!doctype html>\n<html lang="en">\n<head>\n'
        '<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n'
        '<title>Free design systems, compiled by the engine | ux-skill</title>\n'
        '<meta name="description" content="Browse and download free, accessible design systems generated by '
        'the ux-skill engine. Each ships as a full folder (DESIGN.md, tokens, metadata, live preview) and '
        'passes WCAG AA by construction. MIT, offline, no LLM.">\n'
        '<link rel="canonical" href="https://uxskill.laithjunaidy.com/design-systems.html">\n'
        '<meta property="og:type" content="website">\n'
        '<meta property="og:title" content="Free design systems, compiled by the engine">\n'
        '<meta property="og:description" content="Browse and download free, accessible design systems '
        'generated by the ux-skill engine. Full folder + live preview, WCAG AA by construction.">\n'
        '<meta property="og:url" content="https://uxskill.laithjunaidy.com/design-systems.html">\n'
        '<meta name="twitter:card" content="summary_large_image">\n'
        f'{fonts}\n{styles}\n{_GALLERY_CSS}\n'
        f'<script type="application/ld+json">{jsonld}</script>\n'
        '</head>'
    )

    main = (
        '<main class="ds-main">'
        '<section class="ds-hero ds-wrap">'
        '<p class="ds-eyebrow">Design systems</p>'
        '<h1>Free design systems, compiled by the engine.</h1>'
        '<p class="ds-lede">Browse and download ready-to-use design systems. Each one is generated by the '
        'ux-skill engine, ships as a full folder (DESIGN.md, tokens, metadata, and a live preview), and '
        'passes the 152-rule linter. Every text pair meets WCAG AA by construction.</p>'
        '<div class="ds-cta-row">'
        '<a class="ds-cta" href="/commands.html">Use the engine</a>'
        f'<a class="ds-cta ds-cta--ghost" href="#systems">Browse {n} systems</a>'
        '</div>'
        '<p class="ds-pitch">Others sell design systems. ux-skill compiles them, free, and every one passes '
        'the linter. MIT licensed, offline, never calls an LLM.</p>'
        '</section>'
        f'<section id="systems" class="ds-wrap"><div class="ds-grid">{"".join(_card(s) for s in systems)}</div></section>'
        '<section class="ds-wrap ds-how">'
        '<h2>Make your own in one command</h2>'
        '<p>Every system here was generated, not curated. Point the engine at a brief and it synthesizes a '
        'fresh, accessible design language and writes the same folder:</p>'
        '<code class="ds-code">ux system-pack --industry fintech-payments --tone bold --name "Cobalt"</code>'
        '<p>Deterministic, offline, free. <a href="/commands.html">See the commands</a>, read '
        '<a href="/what-is-a-design-md.html">what a DESIGN.md is</a>, or grab the '
        '<a href="design-systems/index.json">machine-readable index</a>.</p>'
        '</section>'
        '</main>'
    )

    page = head_out + nav + main + tail
    Path("docs/design-systems.html").write_text(page, encoding="utf-8")
    return len(page)


_DETAIL_CSS = """<style id="ds-detail">
.dd-wrap{max-width:920px;margin:0 auto;padding:0 24px}
.dd-back{display:inline-flex;align-items:center;gap:7px;font:600 13px ui-monospace,monospace;color:var(--accent,#e8c33a);text-decoration:none;margin-top:40px}
.dd-hero{padding:22px 0 8px}
.dd-tags{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:16px}
.dd-tags span{font:600 11px ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;color:#c2c6d0;border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:4px 11px}
.dd-name{font-size:clamp(40px,7vw,66px);line-height:1.02;letter-spacing:-.025em;margin:0 0 14px}
.dd-desc{font-size:19px;line-height:1.55;opacity:.82;max-width:60ch;margin:0 0 22px}
.dd-hero__actions{display:flex;gap:12px;flex-wrap:wrap}
.dd-cta{display:inline-flex;align-items:center;gap:8px;font-weight:600;font-size:15px;padding:12px 22px;border-radius:10px;text-decoration:none;background:var(--accent,#e8c33a);color:#0a0b0d}
.dd-cta--ghost{background:transparent;color:inherit;border:1px solid rgba(255,255,255,.18)}
.dd-sec{padding:38px 0;border-top:1px solid rgba(255,255,255,.08)}
.dd-label{font:600 12px ui-monospace,monospace;letter-spacing:.14em;text-transform:uppercase;color:var(--accent,#e8c33a);margin:0 0 20px}
.dd-frame{border:1px solid rgba(255,255,255,.12);border-radius:14px;overflow:hidden;background:#000;height:560px}
.dd-frame iframe{width:100%;height:100%;border:0;display:block}
.dd-concept p{font-size:18px;line-height:1.7;max-width:64ch;opacity:.9;margin:0}
.dd-pal{display:flex;flex-direction:column;gap:1px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.1)}
.dd-pal__row{display:grid;grid-template-columns:44px 1.1fr 1fr 1.7fr auto;align-items:center;gap:14px;padding:11px 14px;background:rgba(255,255,255,.02)}
.dd-pal__sw{width:32px;height:32px;border-radius:8px;border:1px solid rgba(255,255,255,.14)}
.dd-pal__name{font-weight:600;font-size:14px}
.dd-pal__hex{font:500 13px ui-monospace,monospace;opacity:.75}
.dd-pal__role{font-size:13px;opacity:.6}
.dd-pal__cr{font:600 12px ui-monospace,monospace;color:#86c79b;justify-self:end}
.dd-type__row{padding:18px 0;border-bottom:1px solid rgba(255,255,255,.06)}
.dd-type__lbl{font:600 11px ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase;opacity:.5}
.dd-type__big{font-size:54px;line-height:1.1;letter-spacing:-.02em;margin:6px 0 2px}
.dd-type__line{font-size:18px;opacity:.8;margin:0}
.dd-a11y{display:flex;flex-direction:column;gap:1px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.1)}
.dd-a11y__row{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:16px;padding:12px 14px;background:rgba(255,255,255,.02);font-size:14px}
.dd-a11y__row b{font:600 14px ui-monospace,monospace}
.dd-pass{font:600 11px ui-monospace,monospace;color:#0a0b0d;background:#86c79b;border-radius:6px;padding:3px 8px}
.dd-files{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px}
.dd-file{display:inline-flex;align-items:center;gap:8px;font:600 13px system-ui,sans-serif;text-decoration:none;color:#e8e9ee;border:1px solid rgba(255,255,255,.14);border-radius:9px;padding:9px 14px}
.dd-file:hover{border-color:rgba(255,255,255,.32)}
.dd-code{font:500 12.5px ui-monospace,monospace;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:18px;overflow:auto;max-height:440px;line-height:1.55;color:#d6d8de;white-space:pre}
.dd-how2 p{font-size:16px;line-height:1.7;opacity:.85;max-width:62ch}
.dd-how2 code{font:500 13px ui-monospace,monospace;background:rgba(255,255,255,.07);padding:2px 6px;border-radius:5px}
.dd-nav{display:flex;justify-content:space-between;gap:16px;padding:34px 0 80px;font-weight:600;flex-wrap:wrap}
.dd-nav a{color:var(--accent,#e8c33a);text-decoration:none}
.usknav__drawer{display:none}
.usknav__drawer.is-open{display:flex;flex-direction:column}
@media (max-width:640px){.dd-pal__row{grid-template-columns:36px 1fr auto;gap:10px}.dd-pal__hex,.dd-pal__role{display:none}.dd-frame{height:460px}}
</style>"""


def build_detail_page(row, prev_row, next_row):
    import html as _html
    styles, fonts, nav, tail = _chrome()
    slug, name = row["slug"], row["name"]
    folder = OUT / slug
    meta = json.loads((folder / "metadata.json").read_text(encoding="utf-8"))
    dmd = _html.escape((folder / "DESIGN.md").read_text(encoding="utf-8"))
    sysfonts = "".join(f'<link href="{f["cssImport"]}" rel="stylesheet">'
                       for f in meta.get("fonts", []) if f.get("cssImport"))
    tags = "".join(f"<span>{t}</span>" for t in meta.get("tags", []))
    pal_rows = "".join(
        f'<div class="dd-pal__row"><span class="dd-pal__sw" style="background:{c["value"]}"></span>'
        f'<span class="dd-pal__name">{c["name"]}</span><code class="dd-pal__hex">{c["value"]}</code>'
        f'<span class="dd-pal__role">{c.get("role", "")}</span>'
        f'<span class="dd-pal__cr">{c.get("contrastOnCanvas", "")}:1</span></div>'
        for c in meta.get("palette", []))
    type_rows = "".join(
        f'<div class="dd-type__row" style="font-family:&#39;{f["name"]}&#39;,system-ui">'
        f'<div class="dd-type__lbl">{f.get("usage", "")} / {f["name"]}</div>'
        f'<div class="dd-type__big">Ag 1234</div>'
        f'<p class="dd-type__line">The quick brown fox jumps over the lazy dog.</p></div>'
        for f in meta.get("fonts", []))
    a11y_rows = "".join(
        f'<div class="dd-a11y__row"><span>{k.replace("_", " ")}</span><b>{v}:1</b>'
        f'<span class="dd-pass">{"AA" if v >= 4.5 else "AA large"}</span></div>'
        for k, v in meta.get("accessibility", {}).get("ratios", {}).items())
    file_links = "".join(
        f'<a class="dd-file" href="/design-systems/{slug}/{path}">{label}</a>'
        for label, path in [("DESIGN.md", "DESIGN.md"), ("tokens.css", "css/tokens.css"),
                            ("metadata.json", "metadata.json"), ("preview.html", "preview.html")])
    prev_link = (f'<a href="/design-systems/{prev_row["slug"]}/">&larr; {prev_row["name"]}</a>'
                 if prev_row else "<span></span>")
    next_link = (f'<a href="/design-systems/{next_row["slug"]}/">{next_row["name"]} &rarr;</a>'
                 if next_row else "<span></span>")
    jsonld = json.dumps({
        "@context": "https://schema.org", "@type": "CreativeWork", "name": f"{name} design system",
        "abstract": meta.get("description", ""), "isAccessibleForFree": True,
        "url": f"https://uxskill.laithjunaidy.com/design-systems/{slug}/",
        "license": "https://opensource.org/licenses/MIT", "keywords": ", ".join(meta.get("tags", [])),
    }, ensure_ascii=False)
    head_out = (
        '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n'
        '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
        f'<title>{name}: a free design system | ux-skill</title>\n'
        f'<meta name="description" content="{name}: {meta.get("description", "")} A free, accessible design '
        'system generated by ux-skill. Download the DESIGN.md, tokens, and a live preview. WCAG AA, MIT.">\n'
        f'<link rel="canonical" href="https://uxskill.laithjunaidy.com/design-systems/{slug}/">\n'
        f'<meta property="og:type" content="website">\n<meta property="og:title" content="{name}: a free design system">\n'
        f'<meta property="og:description" content="{meta.get("description", "")}">\n'
        f'<meta property="og:url" content="https://uxskill.laithjunaidy.com/design-systems/{slug}/">\n'
        '<meta name="twitter:card" content="summary_large_image">\n'
        f'{fonts}\n{sysfonts}\n{styles}\n{_DETAIL_CSS}\n'
        f'<script type="application/ld+json">{jsonld}</script>\n</head>'
    )
    main_html = (
        '<main class="dd"><div class="dd-wrap">'
        '<a class="dd-back" href="/design-systems.html">&larr; All design systems</a>'
        f'<header class="dd-hero"><div class="dd-tags">{tags}</div>'
        f'<h1 class="dd-name">{name}</h1><p class="dd-desc">{meta.get("description", "")}</p>'
        '<div class="dd-hero__actions">'
        f'<a class="dd-cta" href="/design-systems/{slug}.zip" download>Download system</a>'
        f'<a class="dd-cta dd-cta--ghost" href="/design-systems/{slug}/preview.html" target="_blank" rel="noopener">Open full preview</a>'
        '</div></header>'
        '<section class="dd-sec"><div class="dd-label">Live preview</div>'
        f'<div class="dd-frame"><iframe src="/design-systems/{slug}/preview.html" title="{name} preview" loading="lazy"></iframe></div></section>'
        f'<section class="dd-sec dd-concept"><div class="dd-label">Concept</div><p>{meta.get("concept", "")}</p></section>'
        f'<section class="dd-sec"><div class="dd-label">Palette / {len(meta.get("palette", []))} tokens</div><div class="dd-pal">{pal_rows}</div></section>'
        f'<section class="dd-sec"><div class="dd-label">Typography</div><div class="dd-type">{type_rows}</div></section>'
        f'<section class="dd-sec"><div class="dd-label">Accessibility / WCAG AA by construction</div><div class="dd-a11y">{a11y_rows}</div></section>'
        f'<section class="dd-sec"><div class="dd-label">Files</div><div class="dd-files">{file_links}'
        f'<a class="dd-file" href="/design-systems/{slug}.zip" download>Download all (zip)</a></div>'
        f'<pre class="dd-code">{dmd}</pre></section>'
        '<section class="dd-sec dd-how2"><div class="dd-label">How to use</div>'
        '<p>Download the folder and point your coding agent (Claude Code, Cursor, Codex) at '
        '<code>DESIGN.md</code>. Load <code>css/tokens.css</code> for the variables and open '
        '<code>preview.html</code> to see it applied. Generated by the ux-skill engine: deterministic, '
        'offline, never calls an LLM. Free under MIT.</p></section>'
        f'<nav class="dd-nav">{prev_link}<a href="/design-systems.html">All systems</a>{next_link}</nav>'
        '</div></main>'
    )
    (folder / "index.html").write_text(head_out + nav + main_html + tail, encoding="utf-8")
    return slug


def main() -> None:
    if OUT.exists():
        shutil.rmtree(OUT)
    OUT.mkdir(parents=True, exist_ok=True)
    index = []
    for slug, name, brief, description, concept, tags in CATALOG:
        system = synthesize(brief)
        res = pack_system(system, name=name, description=description, concept=concept,
                          out_dir=str(OUT), slug=slug, tags=tags)
        zip_path = OUT / f"{slug}.zip"
        _zip_folder(OUT / slug, zip_path)
        pal = res["palette"]
        index.append({
            "slug": slug, "name": name, "theme": res["theme"], "description": description,
            "display_font": system.type_pair.get("display", "Inter"),
            "body_font": system.type_pair.get("body", "Inter"),
            "tags": tags, "palette": pal,
            "swatches": [pal[k] for k in ("canvas", "surface", "ink", "body", "primary",
                                          "success", "warning", "danger", "info") if k in pal],
            "primary": pal["primary"], "canvas": pal["canvas"], "ink": pal["ink"],
            "contrast": res["ratios"], "wcag_aa": res["wcag_aa_text"],
            "zip": f"{slug}.zip", "files": ["DESIGN.md", "metadata.json", "css/tokens.css", "preview.html"],
        })
        print(f"  {name:10} {res['theme']:5} canvas {pal['canvas']} primary {pal['primary']} "
              f"AA={res['wcag_aa_text']} zip={zip_path.stat().st_size}B")
    (OUT / "index.json").write_text(json.dumps({"systems": index, "count": len(index)},
                                               indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    print(f"\nWrote {len(index)} systems + index.json to {OUT}/")
    size = build_gallery(index)
    print(f"Wrote docs/design-systems.html ({size} bytes)")
    for i, row in enumerate(index):
        build_detail_page(row, index[i - 1] if i > 0 else None,
                          index[i + 1] if i < len(index) - 1 else None)
    print(f"Wrote {len(index)} detail pages (design-systems/<slug>/index.html)")


if __name__ == "__main__":
    main()
