/**
 * Theme system - design tokens that work SSR and client-side.
 *
 * Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and
 * can be overridden by user CSS. Each theme is a flat token map; the framework
 * ships default `light`/`dark` sets and the user's config deep-merges over them.
 *
 * The server renders both `<html data-theme="...">` and
 * `<html data-accent="...">` from cookies, so the correct theme and accent are
 * present before the first paint. The reserved token key `color-scheme` is
 * emitted as the native CSS property instead of a custom property.
 */

export type ThemeSemanticColor =
  "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "error";
export type ThemeToken =
  | "color-scheme"
  | "color-bg"
  | "color-background"
  | "color-foreground"
  | "color-surface"
  | "color-surface-2"
  | "color-surface-raised"
  | "color-surface-muted"
  | "color-surface-soft"
  | "color-text"
  | "color-text-muted"
  | "color-text-subtle"
  | "color-muted"
  | "color-border"
  | "color-border-strong"
  | "color-code-background"
  | "color-code-surface"
  | "color-code-text"
  | "color-code-muted"
  | "color-code-border"
  | `color-${ThemeSemanticColor}`
  | `color-${ThemeSemanticColor}-${"hover" | "active" | "contrast" | "soft" | "muted" | "text"}`
  | `color-on-${"primary" | "secondary"}`
  | "radius"
  | "radius-sm"
  | "shadow-1"
  | "shadow-sm"
  | "shadow-md"
  | "shadow-lg"
  | "space-section"
  | "space-section-sm"
  | "container-max"
  | "font-sans";

/** Known tokens get autocomplete while applications may add namespaced custom tokens. */
export type ThemeTokens = Partial<Record<ThemeToken, string>> & Record<string, string>;

export function defineThemeTokens<T extends ThemeTokens>(tokens: T): T {
  return tokens;
}

export function themeVar(token: ThemeToken, fallback?: string): string {
  return `var(--wire-${token}${fallback ? `, ${fallback}` : ""})`;
}

export const THEME_PALETTE_NAMES = [
  "blue",
  "indigo",
  "violet",
  "emerald",
  "cyan",
  "rose",
  "amber",
  "slate",
] as const;

export type ThemePaletteName = (typeof THEME_PALETTE_NAMES)[number];

/** Required semantic colors for a custom application palette. */
export interface CustomThemePalette {
  primary: string;
  primaryHover: string;
  primaryContrast: string;
  secondary: string;
  secondaryHover: string;
  secondaryContrast: string;
  info: string;
  success: string;
  warning: string;
  danger: string;
}

export interface ThemeAccentConfig {
  /**
   * Accent used when no `wire-accent` cookie is present.
   *
   * - Omitted: use the named `palette`, or `blue` when no palette is configured.
   * - `false`: keep the configured base palette until the user explicitly picks an accent.
   */
  default?: ThemePaletteName | false;
  /** Runtime-selectable accent names. Defaults to every built-in THEME_PALETTE. */
  options?: ThemePaletteName[];
}

export interface ThemeConfig {
  /** Built-in palette name, or a complete custom semantic color palette. */
  palette?: ThemePaletteName | CustomThemePalette;
  /** Runtime accent/palette switcher configuration. */
  accent?: ThemeAccentConfig;
  /** Name of the theme used when no `wire-theme` cookie is present. */
  default?: string;
  /** Named token maps. Deep-merged over the framework's built-in light/dark. */
  themes?: Record<string, ThemeTokens>;
}

export interface ResolvedTheme {
  default: string;
  names: string[];
  themes: Record<string, ThemeTokens>;
  defaultAccent?: ThemePaletteName;
  accentNames: ThemePaletteName[];
}

/** Cookies used by the SSR renderer and client runtime. */
export const THEME_COOKIE = "wire-theme";
export const ACCENT_COOKIE = "wire-accent";
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
export const THEME_CSS_PREFIX = "/__wrnexus/theme/";
export const THEME_JS_HREF = "/__wrnexus/theme.js";

/**
 * Single source of truth for both configured palettes and runtime accents.
 * Do not create a second hard-coded ACCENTS map in the browser runtime.
 */
export const THEME_PALETTES: Record<ThemePaletteName, CustomThemePalette> = {
  blue: {
    primary: "#2563eb",
    primaryHover: "#1d4ed8",
    primaryContrast: "#ffffff",
    secondary: "#475569",
    secondaryHover: "#334155",
    secondaryContrast: "#ffffff",
    info: "#0284c7",
    success: "#16a34a",
    warning: "#d97706",
    danger: "#dc2626",
  },
  indigo: {
    primary: "#4f46e5",
    primaryHover: "#4338ca",
    primaryContrast: "#ffffff",
    secondary: "#7c3aed",
    secondaryHover: "#6d28d9",
    secondaryContrast: "#ffffff",
    info: "#0284c7",
    success: "#16a34a",
    warning: "#d97706",
    danger: "#dc2626",
  },
  violet: {
    primary: "#7c3aed",
    primaryHover: "#6d28d9",
    primaryContrast: "#ffffff",
    secondary: "#db2777",
    secondaryHover: "#be185d",
    secondaryContrast: "#ffffff",
    info: "#2563eb",
    success: "#059669",
    warning: "#d97706",
    danger: "#dc2626",
  },
  emerald: {
    primary: "#059669",
    primaryHover: "#047857",
    primaryContrast: "#ffffff",
    secondary: "#0f766e",
    secondaryHover: "#115e59",
    secondaryContrast: "#ffffff",
    info: "#0284c7",
    success: "#16a34a",
    warning: "#d97706",
    danger: "#dc2626",
  },
  cyan: {
    primary: "#0891b2",
    primaryHover: "#0e7490",
    primaryContrast: "#ffffff",
    secondary: "#2563eb",
    secondaryHover: "#1d4ed8",
    secondaryContrast: "#ffffff",
    info: "#0284c7",
    success: "#16a34a",
    warning: "#d97706",
    danger: "#dc2626",
  },
  rose: {
    primary: "#e11d48",
    primaryHover: "#be123c",
    primaryContrast: "#ffffff",
    secondary: "#9333ea",
    secondaryHover: "#7e22ce",
    secondaryContrast: "#ffffff",
    info: "#2563eb",
    success: "#059669",
    warning: "#d97706",
    danger: "#dc2626",
  },
  amber: {
    primary: "#d97706",
    primaryHover: "#b45309",
    primaryContrast: "#ffffff",
    secondary: "#92400e",
    secondaryHover: "#78350f",
    secondaryContrast: "#ffffff",
    info: "#0284c7",
    success: "#15803d",
    warning: "#d97706",
    danger: "#dc2626",
  },
  slate: {
    primary: "#475569",
    primaryHover: "#334155",
    primaryContrast: "#ffffff",
    secondary: "#0f172a",
    secondaryHover: "#020617",
    secondaryContrast: "#ffffff",
    info: "#0369a1",
    success: "#15803d",
    warning: "#b45309",
    danger: "#b91c1c",
  },
};

type ColorScheme = "light" | "dark";

function colorMix(color: string, amount: number, base: "white" | "black"): string {
  return `color-mix(in srgb, ${color} ${amount}%, ${base})`;
}

function semanticColorTokens(
  name: "primary" | "secondary" | "info" | "success" | "warning" | "danger",
  color: string,
  scheme: ColorScheme,
): ThemeTokens {
  const dark = scheme === "dark";

  return {
    [`color-${name}-soft`]: colorMix(color, dark ? 16 : 8, dark ? "black" : "white"),
    [`color-${name}-muted`]: colorMix(color, dark ? 28 : 16, dark ? "black" : "white"),
    [`color-${name}-text`]: dark ? colorMix(color, 64, "white") : colorMix(color, 82, "black"),
    /*
     * hover and contrast are generated for every semantic colour, not just
     * primary and secondary. Components referenced --wire-color-danger-hover
     * and the rest for a long time with nothing defining them, so those states
     * simply did not paint.
     *
     * primary and secondary override these from the palette, which is why
     * this is spread before their explicit entries rather than after.
     */
    [`color-${name}-hover`]: dark ? colorMix(color, 84, "white") : colorMix(color, 88, "black"),
    [`color-${name}-contrast`]: "#ffffff",
  };
}

function paletteTokens(palette: CustomThemePalette, scheme: ColorScheme): ThemeTokens {
  return {
    // Spread first so the palette's own hover and contrast win over the
    // derived defaults below it.
    ...semanticColorTokens("primary", palette.primary, scheme),
    "color-primary": palette.primary,
    "color-primary-hover": palette.primaryHover,
    "color-primary-active": palette.primaryHover,
    "color-primary-contrast": palette.primaryContrast,
    "color-on-primary": palette.primaryContrast,

    ...semanticColorTokens("secondary", palette.secondary, scheme),
    "color-secondary": palette.secondary,
    "color-secondary-hover": palette.secondaryHover,
    "color-secondary-active": palette.secondaryHover,
    "color-secondary-contrast": palette.secondaryContrast,
    "color-on-secondary": palette.secondaryContrast,

    "color-info": palette.info,
    ...semanticColorTokens("info", palette.info, scheme),

    "color-success": palette.success,
    ...semanticColorTokens("success", palette.success, scheme),

    "color-warning": palette.warning,
    ...semanticColorTokens("warning", palette.warning, scheme),

    "color-danger": palette.danger,
    "color-error": palette.danger,
    ...semanticColorTokens("danger", palette.danger, scheme),
    "color-error-soft": colorMix(
      palette.danger,
      scheme === "dark" ? 16 : 8,
      scheme === "dark" ? "black" : "white",
    ),
    "color-error-muted": colorMix(
      palette.danger,
      scheme === "dark" ? 28 : 16,
      scheme === "dark" ? "black" : "white",
    ),
    "color-error-text":
      scheme === "dark"
        ? colorMix(palette.danger, 64, "white")
        : colorMix(palette.danger, 82, "black"),

    /*
     * Tokens the components have always referenced but nothing defined.
     *
     * An undefined custom property does not warn -- it resolves to nothing --
     * so every focus ring drew with no colour and every soft surface rendered
     * transparent. Derived here so they follow the palette and the accent
     * rather than being pinned per scheme.
     */
    // The palette has no danger contrast; danger is saturated in both schemes,
    // so white is right either way.
    "color-on-danger": "#ffffff",
    "color-focus": palette.primary,
    "color-surface-subtle": colorMix(
      palette.primary,
      scheme === "dark" ? 6 : 4,
      scheme === "dark" ? "black" : "white",
    ),
    "color-input-background": scheme === "dark" ? "#0f0f0f" : "#ffffff",
    "color-input-text": scheme === "dark" ? "#f5f5f5" : "#0b1020",
    "color-input-placeholder": scheme === "dark" ? "#8a8a8a" : "#737b91",
    "color-input-border": scheme === "dark" ? "#ffffff26" : "#d7dced",
    "color-input-border-hover": scheme === "dark" ? "#ffffff38" : "#c7cedd",
    "color-input-border-focus": palette.primary,
  };
}

function resolvePalette(value?: ThemeConfig["palette"]): CustomThemePalette {
  if (!value) return THEME_PALETTES.blue;

  if (typeof value === "string") {
    const palette = THEME_PALETTES[value];
    if (!palette) {
      throw new Error(
        `Unknown theme palette "${value}". Choose one of: ${THEME_PALETTE_NAMES.join(", ")}.`,
      );
    }
    return palette;
  }

  const missing = Object.keys(THEME_PALETTES.blue).filter((key) => {
    const color = value[key as keyof CustomThemePalette];
    return typeof color !== "string" || !color.trim();
  });

  if (missing.length) {
    throw new Error(`Custom theme palette is missing required colors: ${missing.join(", ")}.`);
  }

  return value;
}

function uniqueAccentNames(values?: ThemePaletteName[]): ThemePaletteName[] {
  if (!values?.length) return [...THEME_PALETTE_NAMES];

  const allowed = new Set<ThemePaletteName>();
  for (const value of values) {
    if (THEME_PALETTE_NAMES.includes(value)) allowed.add(value);
  }

  return allowed.size ? [...allowed] : [...THEME_PALETTE_NAMES];
}

function resolveDefaultAccent(
  config: ThemeConfig | undefined,
  accentNames: ThemePaletteName[],
): ThemePaletteName | undefined {
  if (config?.accent?.default === false) return undefined;

  const explicit = config?.accent?.default;
  if (explicit && accentNames.includes(explicit)) return explicit;

  const palette = config?.palette;
  if (typeof palette === "string" && accentNames.includes(palette)) return palette;

  if (!palette && accentNames.includes("blue")) return "blue";

  return undefined;
}

/** Built-in themes so components have tokens out of the box. */
export const DEFAULT_THEMES: Record<string, ThemeTokens> = {
  light: {
    "color-scheme": "light",
    "color-bg": "#ffffff",
    "color-background": "#ffffff",
    "color-foreground": "#0b1020",
    "color-surface-raised": "#ffffff",
    "color-surface-muted": "#eceff6",
    "color-surface-soft": "#f1f4f9",
    "color-text-muted": "#5a6178",
    "color-text-subtle": "#737b91",
    "color-border-strong": "#c7cedd",
    "color-code-background": "#0b1020",
    "color-code-surface": "#141a30",
    "color-code-text": "#e7ecff",
    "color-code-muted": "#9aa6d0",
    "color-code-border": "#27304d",
    "shadow-sm": "0 1px 2px rgba(16,24,40,0.06)",
    "shadow-md": "0 8px 24px rgba(16,24,40,0.10)",
    "shadow-lg": "0 20px 50px rgba(16,24,40,0.14)",
    "space-section": "5rem",
    "space-section-sm": "3.5rem",
    "container-max": "80rem",
    "color-surface": "#f6f7fb",
    "color-surface-2": "#eceff6",
    "color-text": "#0b1020",
    "color-muted": "#5a6178",
    "color-border": "#e2e6f0",
    ...paletteTokens(THEME_PALETTES.blue, "light"),
    radius: "8px",
    "radius-sm": "5px",
    "font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
    "shadow-1": "0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.1)",
  },
  dark: {
    "color-scheme": "dark",
    "color-bg": "#000000",
    "color-background": "#000000",
    "color-foreground": "#f5f5f5",
    "color-surface-raised": "#111111",
    "color-surface-muted": "#181818",
    "color-surface-soft": "#141414",
    "color-text-muted": "#b3b3b3",
    "color-text-subtle": "#8a8a8a",
    "color-border-strong": "#ffffff38",
    "color-code-background": "#050505",
    "color-code-surface": "#101010",
    "color-code-text": "#f5f5f5",
    "color-code-muted": "#a3a3a3",
    "color-code-border": "#ffffff24",
    "shadow-sm": "0 1px 2px rgba(0,0,0,0.28)",
    "shadow-md": "0 10px 30px rgba(0,0,0,0.35)",
    "shadow-lg": "0 24px 64px rgba(0,0,0,0.45)",
    "space-section": "5rem",
    "space-section-sm": "3.5rem",
    "container-max": "80rem",
    "color-surface": "#0a0a0a",
    "color-surface-2": "#171717",
    "color-text": "#f5f5f5",
    "color-muted": "#a3a3a3",
    "color-border": "#ffffff1f",
    ...paletteTokens(THEME_PALETTES.blue, "dark"),
    radius: "10px",
    "radius-sm": "6px",
    "font-sans": '"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif',
    "shadow-1": "0 1px 2px rgba(0,0,0,0.3), 0 4px 16px rgba(0,0,0,0.35)",
  },
};

function themeScheme(tokens: ThemeTokens): ColorScheme {
  return tokens["color-scheme"] === "dark" ? "dark" : "light";
}

/** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
  const palette = resolvePalette(config?.palette);
  const themes: Record<string, ThemeTokens> = {};
  const names = new Set<string>([
    ...Object.keys(DEFAULT_THEMES),
    ...Object.keys(config?.themes ?? {}),
  ]);

  for (const name of names) {
    const base = DEFAULT_THEMES[name] ?? DEFAULT_THEMES.light;
    const scheme = themeScheme(base);

    themes[name] = {
      ...base,
      ...paletteTokens(palette, scheme),
      ...(config?.themes?.[name] ?? {}),
    };
  }

  const list = Object.keys(themes);
  const preferred = config?.default && themes[config.default] ? config.default : undefined;
  const fallback = themes.dark ? "dark" : list[0]!;
  const accentNames = uniqueAccentNames(config?.accent?.options);
  const defaultAccent = resolveDefaultAccent(config, accentNames);

  return {
    default: preferred ?? fallback,
    names: list,
    themes,
    defaultAccent,
    accentNames,
  };
}

/** Pick a valid theme name from a cookie value, falling back to the default. */
export function resolveThemeName(cookieValue: string | undefined, theme: ResolvedTheme): string {
  return cookieValue && theme.themes[cookieValue] ? cookieValue : theme.default;
}

/** Pick a valid accent name from a cookie value, falling back to the configured default. */
export function resolveAccentName(
  cookieValue: string | undefined,
  theme: ResolvedTheme,
): ThemePaletteName | undefined {
  if (cookieValue && theme.accentNames.includes(cookieValue as ThemePaletteName)) {
    return cookieValue as ThemePaletteName;
  }

  return theme.defaultAccent;
}

function tokensToDeclarations(tokens: ThemeTokens): string {
  return Object.entries(tokens)
    .map(([key, value]) =>
      key === "color-scheme" ? `color-scheme:${value};` : `--wire-${key}:${value};`,
    )
    .join("");
}

function selectorValue(value: string): string {
  return JSON.stringify(value);
}

function globalThemeCss(): string {
  return (
    '\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
    ":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
    "--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
    "0 10px 30px color-mix(in srgb, black 14%, transparent);}\n" +
    "wrn-slot{display:contents;}\n" +
    "[data-for]{display:none !important;}\n"
  );
}

/** URL for the small stylesheet containing only one active theme/accent pair. */
export function activeThemeCssHref(themeName: string, accentName?: string): string {
  return `${THEME_CSS_PREFIX}${encodeURIComponent(themeName)}/${accentName ? encodeURIComponent(accentName) : "_"}.css`;
}

/** Render only the tokens needed for the current SSR-selected theme and accent. */
export function renderActiveThemeCss(
  theme: ResolvedTheme,
  themeName: string,
  accentName?: string,
): string {
  const selectedTheme = theme.themes[themeName];
  if (!selectedTheme) throw new Error(`Unknown theme '${themeName}'.`);
  const blocks = [`:root{${tokensToDeclarations(selectedTheme)}}`];
  if (accentName) {
    if (!theme.accentNames.includes(accentName as ThemePaletteName)) {
      throw new Error(`Unknown theme accent '${accentName}'.`);
    }
    blocks.push(
      `:root{${tokensToDeclarations(
        paletteTokens(THEME_PALETTES[accentName as ThemePaletteName], themeScheme(selectedTheme)),
      )}}`,
    );
  }
  return blocks.join("\n") + globalThemeCss();
}

/**
 * Generate the theme stylesheet.
 *
 * Theme selectors are emitted first. Accent selectors are emitted afterwards,
 * so a selected accent consistently overrides every semantic palette token,
 * including soft/muted/text variants, before the first paint.
 */
export function renderThemeCss(theme: ResolvedTheme): string {
  const blocks: string[] = [];
  const defaultTheme = theme.themes[theme.default];

  if (defaultTheme) blocks.push(`:root{${tokensToDeclarations(defaultTheme)}}`);

  for (const name of theme.names) {
    blocks.push(
      `[data-theme=${selectorValue(name)}]{${tokensToDeclarations(theme.themes[name]!)}}`,
    );
  }

  for (const accentName of theme.accentNames) {
    const palette = THEME_PALETTES[accentName];

    if (defaultTheme) {
      blocks.push(
        `:root[data-accent=${selectorValue(accentName)}]{${tokensToDeclarations(
          paletteTokens(palette, themeScheme(defaultTheme)),
        )}}`,
      );
    }

    for (const themeName of theme.names) {
      const themeTokens = theme.themes[themeName]!;
      blocks.push(
        `[data-theme=${selectorValue(themeName)}][data-accent=${selectorValue(
          accentName,
        )}]{${tokensToDeclarations(paletteTokens(palette, themeScheme(themeTokens)))}}`,
      );
    }
  }

  return blocks.join("\n") + globalThemeCss();
}

/**
 * Generate the client theme runtime. It exposes `window.wireTheme` and
 * `window.wireAccent`, and binds theme/accent controls.
 *
 * The runtime changes only data attributes and cookies. It never writes inline
 * CSS variables and never uses localStorage, so CSS and SSR remain the single
 * source of truth.
 */
export function renderThemeRuntime(theme: ResolvedTheme): string {
  const names = JSON.stringify(theme.names);
  const accentNames = JSON.stringify(theme.accentNames);
  const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
  const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);

  return `(function(){
  var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
  var ACCENT_COOKIE=${JSON.stringify(ACCENT_COOKIE)};
  var THEMES=${names};
  var ACCENTS=${accentNames};
  var DEFAULT_THEME=${JSON.stringify(theme.default)};
  var DEFAULT_ACCENT=${defaultAccent};
  var THEME_CSS_PREFIX=${cssPrefix};
  var MAX_AGE=31536000;
  var el=document.documentElement;

  function readCookie(name){
    var prefix=encodeURIComponent(name)+"=";
    var parts=document.cookie?document.cookie.split(";"):[];
    for(var i=0;i<parts.length;i++){
      var part=parts[i].trim();
      if(part.indexOf(prefix)!==0)continue;
      var value=part.slice(prefix.length);
      try{return decodeURIComponent(value);}catch(_){return value;}
    }
    return null;
  }

  function writeCookie(name,value){
    var secure=location.protocol==="https:"?";secure":"";
    document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+
      ";path=/;max-age="+MAX_AGE+";samesite=lax"+secure;
  }

  function deleteCookie(name){
    var secure=location.protocol==="https:"?";secure":"";
    document.cookie=encodeURIComponent(name)+"=;path=/;max-age=0;samesite=lax"+secure;
  }

  function syncStylesheet(themeName,accentName){
    var link=document.querySelector("link[data-wrnexus-theme]");
    if(!link)return;
    var accent=accentName?encodeURIComponent(accentName):"_";
    link.href=THEME_CSS_PREFIX+encodeURIComponent(themeName)+"/"+accent+".css";
  }

  function getTheme(){
    var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
    return THEMES.indexOf(current)>=0?current:DEFAULT_THEME;
  }

  function getAccent(){
    var current=el.getAttribute("data-accent")||readCookie(ACCENT_COOKIE)||DEFAULT_ACCENT;
    return current&&ACCENTS.indexOf(current)>=0?current:null;
  }

  function syncThemeButtons(root){
    var current=getTheme();
    (root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(node){
      node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-theme-set")===current));
    });
  }

  function syncAccentButtons(root){
    var current=getAccent();
    (root||document).querySelectorAll("[data-wire-accent-set]").forEach(function(node){
      node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-accent-set")===current));
    });
  }

  function setTheme(name){
    if(THEMES.indexOf(name)<0)return;
    el.setAttribute("data-theme",name);
    writeCookie(THEME_COOKIE,name);
    syncStylesheet(name,getAccent());
    syncThemeButtons(document);
    window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
  }

  function toggleTheme(){
    var index=THEMES.indexOf(getTheme());
    setTheme(THEMES[(index+1)%THEMES.length]);
  }

  function setAccent(name){
    if(ACCENTS.indexOf(name)<0)return;
    el.setAttribute("data-accent",name);
    writeCookie(ACCENT_COOKIE,name);
    syncStylesheet(getTheme(),name);
    syncAccentButtons(document);
    window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
  }

  function clearAccent(){
    el.removeAttribute("data-accent");
    deleteCookie(ACCENT_COOKIE);
    syncStylesheet(getTheme(),null);
    syncAccentButtons(document);
    window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
  }

  function bind(root){
    var target=root||document;

    target.querySelectorAll("[data-wire-theme-toggle]").forEach(function(node){
      if(node.__wireThemeBound)return;
      node.__wireThemeBound=1;
      node.addEventListener("click",toggleTheme);
    });

    target.querySelectorAll("[data-wire-theme-set]").forEach(function(node){
      if(node.__wireThemeBound)return;
      node.__wireThemeBound=1;
      node.addEventListener("click",function(){
        setTheme(node.getAttribute("data-wire-theme-set"));
      });
    });

    target.querySelectorAll("[data-wire-accent-set]").forEach(function(node){
      if(node.__wireAccentBound)return;
      node.__wireAccentBound=1;
      node.addEventListener("click",function(){
        setAccent(node.getAttribute("data-wire-accent-set"));
      });
    });

    target.querySelectorAll("[data-wire-accent-clear]").forEach(function(node){
      if(node.__wireAccentBound)return;
      node.__wireAccentBound=1;
      node.addEventListener("click",clearAccent);
    });

    syncThemeButtons(target);
    syncAccentButtons(target);
  }

  // SSR should already have these attributes. The cookie fallback only protects
  // manually generated/static documents that did not pass through WRNexus SSR.
  var initialTheme=getTheme();
  if(!el.hasAttribute("data-theme"))el.setAttribute("data-theme",initialTheme);
  var initialAccent=getAccent();
  if(initialAccent&&!el.hasAttribute("data-accent"))el.setAttribute("data-accent",initialAccent);

  window.wireTheme={
    get:getTheme,
    set:setTheme,
    toggle:toggleTheme,
    bind:bind,
    themes:THEMES.slice()
  };

  window.wireAccent={
    get:getAccent,
    set:setAccent,
    clear:clearAccent,
    colors:ACCENTS.slice()
  };

  window.addEventListener("wrnexus:navigated",function(){bind(document);});
  if(document.readyState==="loading"){
    document.addEventListener("DOMContentLoaded",function(){bind(document);});
  }else{
    bind(document);
  }
})();
`;
}
