// Settings › Brand — the layout colours card, as the approved Brand prototype draws it.
//
// Omar, 2026-10-21 (wiki Brand_design §2 D31): "The Brand section must look like the
// prototype (screens drawn by the real course player, cards that open and close, the
// prototype's colour names, only the colours of the chosen view)." And D32: "New" colours
// are hidden until C makes them real, "so the preview only shows what learners receive".
//
// What it draws: the screens the prototype showed — markup and style sheets of the RELEASED
// course player, captured for every layout and view (`GET /v1/org/brand/preview`, see
// routes/brand-preview.ts). Each layout colour was painted a sentinel `rgb(7, <layout>, <k>)`
// when captured, so the page finds what each colour paints and repaints it from the screen's
// brand (`brand-preview-core.js`). Course-wide colours are painted with the EXPORT's own rules.
//
// The engine below is the prototype's (round 3, which Omar approved as round 5), moved into
// React's lifecycle: one frame, booted once with the player's style sheets; a change of
// layout or view swaps the markup and re-reads what is visible; a change of colour only
// repaints. Nothing here decides a colour: the screen hands in what the brand holds.

const BRAND_PREVIEW_WIDTH = 1280;
const brandPreviewCache = { data: null, promise: null };

/** The previews, fetched once per page (they are the same for every workspace). */
function loadBrandPreview() {
  if (brandPreviewCache.data) return Promise.resolve(brandPreviewCache.data);
  if (!brandPreviewCache.promise) {
    brandPreviewCache.promise = (async () => {
      if (typeof window.dynamoGetAccessToken !== 'function') throw new Error('not signed in');
      const token = await window.dynamoGetAccessToken();
      const res = await fetch(`${(window.DYNAMO_ENV || {}).gatewayBase}/v1/org/brand/preview`, {
        headers: { authorization: `Bearer ${token}` },
      });
      if (!res.ok) throw new Error(`the server refused (${res.status})`);
      const data = await res.json();
      brandPreviewCache.data = data;
      return data;
    })();
    // a failure may be tried again on the next visit
    brandPreviewCache.promise.catch(() => { brandPreviewCache.promise = null; });
  }
  return brandPreviewCache.promise;
}

// What a NEW screen of a layout keeps for a colour its stored set lacks (D6/D7: a partly stored layout keeps its
// gaps): the layout's own starting value — the blank skeleton's, which `createLayout` leaves in place. Where the
// skeleton carries no colour there, the starting colour, as for a layout with nothing stored:
//   · fair for an item a new screen does not have yet (a hotspot the author adds later, which Brand does not colour);
//   · ★ a KNOWN LIMIT for 18 colours on items a new screen DOES have without a colour of its own (Sequence steps, the
//     quiz questions, Quiz · gaming's start, pass and fail screens): there a course shows the course player's own
//     colour, and the preview the starting one. Only layouts saved partly by the screen before 2026-10-21 have such
//     gaps; the screen since never creates one. `fe-brand-preview-gap.test.ts` names all three outcomes.
const brandPreviewSkeletons = {};
function brandPreviewGapColour(type, path, factoryLayouts) {
  if (!(type in brandPreviewSkeletons)) {
    let base = null;
    try { base = typeof window.layoutContentBase === 'function' ? window.layoutContentBase(type, 'blank') : null; } catch { base = null; }
    brandPreviewSkeletons[type] = base;
  }
  const base = brandPreviewSkeletons[type];
  const own = base && typeof window.getBrandPath === 'function' ? window.getBrandPath(base, path) : undefined;
  if (window.BrandPreviewCore.isColour(own)) return own;
  return ((factoryLayouts || {})[type] || {})[path];
}

const BRAND_PREVIEW_PROPS = ['backgroundColor', 'color', 'borderTopColor', 'borderRightColor', 'borderBottomColor',
  'borderLeftColor', 'outlineColor', 'fill', 'stroke', 'textDecorationColor', 'boxShadow', 'backgroundImage'];
const BRAND_PREVIEW_CSS_NAME = { backgroundColor: 'background-color', color: 'color', borderTopColor: 'border-top-color',
  borderRightColor: 'border-right-color', borderBottomColor: 'border-bottom-color', borderLeftColor: 'border-left-color',
  outlineColor: 'outline-color', fill: 'fill', stroke: 'stroke', textDecorationColor: 'text-decoration-color',
  boxShadow: 'box-shadow', backgroundImage: 'background-image' };

/**
 * The frame and everything that touches it — the prototype's script, unchanged in what it
 * does. Created once per card; `ui` is how it tells React what it found.
 */
function createBrandPreviewEngine(frame, ui) {
  const core = window.BrandPreviewCore;
  let data = null, doc = null, entry = null, view = null, paintLayout = null;
  let paints = [];
  let values = { fields: {}, courseWide: {} };

  function boot(d, done) {
    data = d;
    frame.onload = () => {
      doc = frame.contentDocument;
      doc.addEventListener('click', onClick, true);
      done();
    };
    frame.srcdoc = '<!doctype html><html><head><meta charset="utf-8"><style>' + core.detok(data.css, data.assets) + '</style>' +
      '<style>.zz-hl{outline:3px solid #2563eb !important;outline-offset:2px !important;box-shadow:0 0 0 6px rgba(37,99,235,.25) !important}*{cursor:default}</style>' +
      '<style id="zz-cw"></style><style id="zz-pseudo"></style></head><body></body></html>';
  }
  function ready() { return !!doc; }

  function visible(e) {
    const w = frame.contentWindow;
    if (!e.getClientRects().length) return false;
    const cs = w.getComputedStyle(e);
    if (cs.visibility === 'hidden' || cs.display === 'none') return false;
    for (let x = e; x && x !== doc.body; x = x.parentElement) {
      const c = w.getComputedStyle(x);
      if (+c.opacity < 0.05 || c.display === 'none') return false;
    }
    const r = e.getBoundingClientRect();
    return r.width > 1 && r.height > 1;
  }
  function marks(e, attr) {
    const a = e.getAttribute(attr);
    return a ? a.split('|').map(x => { const p = x.split(':'); return { k: +p[0], prop: p[1] }; }) : [];
  }
  const MARK_ATTRS = ['data-zzp', 'data-zzpb', 'data-zzpa'];
  function fieldEls(k) {
    return Array.from(doc.querySelectorAll('[data-zzp],[data-zzpb],[data-zzpa]'))
      .filter(e => MARK_ATTRS.some(a => marks(e, a).some(m => m.k === k)))
      .filter(visible);
  }
  function courseEls(key) {
    const sel = data.courseWide.targets[key];
    if (!sel) return [];
    let list = [];
    try { list = Array.from(doc.querySelectorAll(sel)); } catch { list = []; }
    return list.filter(visible);
  }
  function kOf(path) {
    const f = (paintLayout.fields || []).find(x => x.path === path);
    return f ? f.k : -1;
  }
  function rowEls(key) {
    if (key.indexOf('f:') === 0) return fieldEls(kOf(key.slice(2)));
    if (key.indexOf('c:') === 0) return courseEls(key.slice(2));
    return [];
  }

  function size(box) {
    if (!box || !view) return;
    const s = box.clientWidth / BRAND_PREVIEW_WIDTH, h = Math.max(view.height || 800, 360);
    frame.style.width = BRAND_PREVIEW_WIDTH + 'px';
    frame.style.height = h + 'px';
    frame.style.transform = 'scale(' + s + ')';
    box.style.height = Math.round(h * s) + 'px';
  }

  /** Swap the screen, then read what this view shows. */
  function render(nextEntry, nextView, box) {
    if (!doc) return;
    entry = nextEntry; view = nextView;
    paintLayout = view.paintAs ? data.layouts.find(l => l.type === view.paintAs) || entry : entry;
    doc.documentElement.className = view.htmlCls || '';
    doc.documentElement.setAttribute('style', view.htmlStyle || '');
    doc.body.className = view.bodyCls || '';
    doc.body.setAttribute('style', view.bodyStyle || '');
    doc.body.innerHTML = core.detok(view.inner, data.assets);
    Array.from(doc.querySelectorAll('[data-zzp],[data-zzpb],[data-zzpa]')).forEach((e, i) => e.setAttribute('data-zzi', i));
    scan();
    size(box);
    apply();
    report();
  }

  // Every element whose drawn colour carries one of this layout's sentinels, read ONCE per
  // view before anything is painted (painting rewrites the style the next read would see).
  function scan() {
    paints = [];
    const w = frame.contentWindow, li = paintLayout.index;
    if (li === undefined || li < 0) return;
    Array.from(doc.body.querySelectorAll('*')).forEach(e => {
      const cs = w.getComputedStyle(e);
      BRAND_PREVIEW_PROPS.forEach(p => {
        const v = cs[p];
        const ks = core.sentinelKeys(v, li);
        if (ks.length) paints.push({ e, p, orig: v });
      });
    });
  }

  function apply() {
    if (!doc || !paintLayout) return;
    const li = paintLayout.index;
    const valueOfK = k => {
      const f = (paintLayout.fields || []).find(x => x.k === k);
      return f ? values.fields[f.path] : undefined;
    };
    paints.forEach(x => {
      x.e.style.setProperty(BRAND_PREVIEW_CSS_NAME[x.p] || x.p, core.repaint(x.orig, li, valueOfK), 'important');
    });
    const pseudo = [];
    ['data-zzpb', 'data-zzpa'].forEach(attr => {
      Array.from(doc.querySelectorAll('[' + attr + ']')).forEach(e => {
        const ps = attr === 'data-zzpb' ? '::before' : '::after';
        marks(e, attr).forEach(m => {
          const v = valueOfK(m.k);
          if (!core.isColour(v)) return;
          pseudo.push('[data-zzi="' + e.getAttribute('data-zzi') + '"]' + ps + '{' + (BRAND_PREVIEW_CSS_NAME[m.prop] || m.prop) + ':' + v + ' !important}');
        });
      });
    });
    doc.getElementById('zz-pseudo').textContent = pseudo.join('\n');
    doc.getElementById('zz-cw').textContent = core.courseWideCss(data.courseWide.css, data.courseWide.sentinels, values.courseWide);
  }

  function setValues(v) { values = v; apply(); }

  // what this view shows, for the panel
  function report() {
    const paths = new Set();
    if (entry.type !== 'course') {
      (entry.fields || []).forEach(f => {
        if (f.views && f.views.indexOf(view.id) === -1) return; // a colour kept to one view (the dots)
        if (fieldEls(f.k).length) paths.add(f.path);
      });
    }
    const course = new Set();
    (view.rows || []).forEach(key => { if (courseEls(key).length) course.add(key); });
    ui.visible({ paths, course });
  }

  function highlight(els) {
    if (!doc) return;
    Array.from(doc.querySelectorAll('.zz-hl')).forEach(e => e.classList.remove('zz-hl'));
    (els || []).forEach(e => {
      const t = (e instanceof frame.contentWindow.SVGElement && e.ownerSVGElement) ? (e.ownerSVGElement.parentElement || e)
        : (e.tagName === 'svg' ? e.parentElement : e);
      t.classList.add('zz-hl');
    });
  }
  /** While a pointed-at colour's row is pointed at, the part shows that state. */
  function force(courseKey, on) {
    if (!doc) return;
    const host = data.courseWide.hoverHosts && data.courseWide.hoverHosts[courseKey];
    if (!host) return;
    let list = [];
    try { list = Array.from(doc.querySelectorAll(host)); } catch { list = []; }
    list.forEach(e => e.classList.toggle(data.courseWide.forceHoverClass, on));
  }

  // clicking the preview finds the colour of what was clicked
  function onClick(ev) {
    ev.preventDefault(); ev.stopPropagation();
    for (let e = ev.target; e && e !== doc.documentElement; e = e.parentElement) {
      const keys = [];
      MARK_ATTRS.forEach(a => marks(e, a).forEach(m => {
        const f = (paintLayout.fields || []).find(x => x.k === m.k);
        if (f) keys.push('f:' + f.path);
      }));
      (view.rows || []).forEach(key => { if (courseEls(key).indexOf(e) !== -1) keys.push('c:' + key); });
      if (ui.find(keys)) { highlight([e]); return; }
    }
  }

  return { boot, ready, render, setValues, size, rowEls, highlight, force };
}

/**
 * The layout colours card. `brand` is the screen's (unsaved changes included); the card reads
 * it and reports changes through the callbacks — it holds no colour of its own.
 */
function BrandLayoutColours({ brand, onLayoutColour, onCourseWide, onResetLayout, onResetCourseWide, disabled, initialEntry }) {
  const core = window.BrandPreviewCore;
  const FIELDS = (typeof BRAND_COLOUR_FIELDS !== 'undefined' && BRAND_COLOUR_FIELDS) || {};
  const TYPES = (typeof BRAND_LAYOUT_TYPES !== 'undefined' && BRAND_LAYOUT_TYPES) || [];
  const NAMES = (typeof BRAND_LAYOUT_NAMES !== 'undefined' && BRAND_LAYOUT_NAMES) || {};
  const COURSE_FIELDS = (typeof BRAND_COURSE_WIDE_FIELDS !== 'undefined' && BRAND_COURSE_WIDE_FIELDS) || [];
  const FACTORY = (typeof FACTORY_WORKSPACE_BRAND !== 'undefined' && FACTORY_WORKSPACE_BRAND) || { courseWide: {}, layouts: {} };

  const [data, setData] = React.useState(brandPreviewCache.data);
  const [loadError, setLoadError] = React.useState(null);
  const [entryType, setEntryType] = React.useState(initialEntry || 'course');
  const [viewIdx, setViewIdx] = React.useState(0);
  const [visible, setVisible] = React.useState(null);
  const [open, setOpen] = React.useState({});
  const [flash, setFlash] = React.useState([]);
  const frameRef = React.useRef(null), boxRef = React.useRef(null), engineRef = React.useRef(null), panelRef = React.useRef(null);
  const [booted, setBooted] = React.useState(false);
  // the last valid colour per row, so a half-typed hex never paints the preview
  const goodRef = React.useRef({ fields: {}, courseWide: {} });

  React.useEffect(() => {
    let alive = true;
    if (!data) loadBrandPreview().then(d => { if (alive) setData(d); }, e => { if (alive) setLoadError((e && e.message) || 'the previews could not be loaded'); });
    return () => { alive = false; };
  }, []);

  const entries = [{ type: 'course', name: 'Course-wide' }].concat(TYPES.map(t => ({ type: t, name: NAMES[t] || t })));
  const captured = data ? data.layouts.find(l => l.type === entryType) : null;
  const views = captured ? captured.views : [];
  const view = views[Math.min(viewIdx, Math.max(views.length - 1, 0))] || null;
  const isCourse = entryType === 'course';
  const paintType = view && view.paintAs ? view.paintAs : entryType;

  const layoutValues = type => (typeof window.effectiveBrandColours === 'function'
    ? (window.effectiveBrandColours(brand, type, FIELDS) || {}) : ((brand.layouts || {})[type] || {}));
  const courseValues = Object.assign({}, FACTORY.courseWide || {}, brand.courseWide || {});

  // values for the frame: only valid colours; a half-typed one keeps the last valid; a colour a partly stored
  // layout lacks paints what a new screen of it gets (D6/D7), never the capture's near-black
  const frameValues = (() => {
    const good = goodRef.current;
    const lastGood = good.fields[paintType] || (good.fields[paintType] = {});
    const fields = core.frameLayoutValues((FIELDS[paintType] || []).map(f => f.path), layoutValues(paintType),
      p => brandPreviewGapColour(paintType, p, FACTORY.layouts), lastGood);
    const cw = {};
    Object.keys(courseValues).forEach(k => { if (core.isColour(courseValues[k])) good.courseWide[k] = courseValues[k]; cw[k] = good.courseWide[k]; });
    return { fields, courseWide: cw };
  })();

  // boot the frame once the previews have come
  React.useEffect(() => {
    if (!data || !frameRef.current) return;
    const engine = createBrandPreviewEngine(frameRef.current, {
      visible: v => setVisible(v),
      // through a ref: the frame lives on across layouts, and must find rows on the CURRENT one
      find: keys => findRef.current(keys),
    });
    engineRef.current = engine;
    engine.boot(data, () => setBooted(true));
  }, [data]);

  // a new layout or view: swap the screen
  React.useEffect(() => {
    const engine = engineRef.current;
    if (!booted || !engine || !captured || !view) return;
    engine.setValues(frameValues);
    engine.render(captured, view, boxRef.current);
  }, [booted, entryType, viewIdx]);

  // a new colour: repaint only
  const valuesKey = JSON.stringify(frameValues);
  React.useEffect(() => {
    const engine = engineRef.current;
    if (booted && engine) engine.setValues(frameValues);
  }, [booted, valuesKey]);

  React.useEffect(() => {
    const onResize = () => { if (engineRef.current) engineRef.current.size(boxRef.current); };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // ── the panel ──
  // Which rows: the colours the frame found visible in this view. Until it has read the view,
  // none (never "all", which would list colours the view does not show). With no previews at
  // all (they could not be loaded), every colour, so they can still be set.
  const cards = (() => {
    if (isCourse) {
      const rows = view ? (view.rows || []) : COURSE_FIELDS.map(f => f.key);
      const shown = data ? (visible ? visible.course : new Set()) : new Set(rows);
      return core.courseCards(rows, shown, COURSE_FIELDS);
    }
    const all = FIELDS[entryType] || [];
    const shown = data ? (visible ? visible.paths : new Set()) : new Set(all.map(f => f.path));
    return core.layoutCards(all, shown);
  })();
  const valueOf = row => (row.courseKey ? courseValues[row.courseKey] : (layoutValues(entryType)[row.path] || ''));
  const isOpen = (title, i) => {
    const o = open[entryType] || {};
    return o[title] !== undefined ? o[title] : core.openByDefault(isCourse, title, i);
  };
  const setCardOpen = (title, value) => setOpen(o => Object.assign({}, o, { [entryType]: Object.assign({}, o[entryType] || {}, { [title]: value }) }));

  const findRef = React.useRef(null);
  findRef.current = findRows;
  function findRows(keys) {
    // from the cards, not the page: a closed card has no rows on the page to find
    const present = keys.filter(k => cards.some(c => c.rows.some(r => r.key === k)));
    if (!present.length) return false;
    cards.forEach(c => { if (c.rows.some(r => present.indexOf(r.key) !== -1)) setCardOpen(c.title, true); });
    setFlash(present);
    setTimeout(() => {
      const row = panelRef.current && panelRef.current.querySelector('[data-row-key="' + present[0] + '"]');
      if (row && row.scrollIntoView) row.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
    }, 50);
    return true;
  }

  const point = (keys, on, row) => {
    const engine = engineRef.current;
    if (!engine || !engine.ready()) return;
    engine.highlight(on ? keys.reduce((els, k) => els.concat(engine.rowEls(k)), []) : []);
    if (row && row.courseKey) engine.force(row.courseKey, on);
  };

  const count = cards.reduce((n, c) => n + c.rows.length, 0);
  const entryName = (entries.find(e => e.type === entryType) || {}).name || entryType;
  const older = data && data.capturedFrom && data.pinnedRuntime && data.capturedFrom !== data.pinnedRuntime;

  const CHEV = <svg className="chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"><path d="M9 6l6 6-6 6" /></svg>;

  return (
    <section className="card brand-layout-card" style={{ padding: '16px 18px' }} data-brand-layout-card="">
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 14 }}>
        <select className="field select-elegant" aria-label="Layout" value={entryType}
          onChange={e => { if (e.target.value === entryType) return; setEntryType(e.target.value); setViewIdx(0); setVisible(null); setFlash([]); }}
          style={{ minWidth: 280 }}>
          <option value="course">Course-wide</option>
          <optgroup label="Layouts">
            {TYPES.map(t => <option key={t} value={t}>{NAMES[t] || t}</option>)}
          </optgroup>
        </select>
        {views.length > 1 && (
          <div role="tablist" aria-label="What the learner sees" style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
            {views.map((v, i) => (
              <button key={v.id} type="button" role="tab" aria-selected={i === viewIdx} className="brand-view-btn"
                onClick={() => { if (i === viewIdx) return; setViewIdx(i); setVisible(null); setFlash([]); }}
                style={{ font: 'inherit', fontSize: 12.5, padding: '6px 11px', borderRadius: 999, cursor: 'pointer',
                  border: '1px solid ' + (i === viewIdx ? 'var(--text)' : 'var(--border)'),
                  background: i === viewIdx ? 'var(--text)' : 'var(--surface)',
                  color: i === viewIdx ? 'var(--surface)' : 'var(--text)' }}>{v.label}</button>
            ))}
          </div>
        )}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 330px', gap: 18, alignItems: 'start' }}>
        <div>
          <div ref={boxRef} data-brand-preview-frame=""
            style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', overflow: 'hidden',
              background: '#000', position: 'relative', minHeight: data ? undefined : 320 }}>
            {data
              ? <iframe ref={frameRef} title="The layout, drawn by the course player"
                  sandbox="allow-same-origin" data-script-free-frame=""
                  style={{ border: 0, width: BRAND_PREVIEW_WIDTH, transformOrigin: '0 0', display: 'block', background: '#000' }} />
              : <div style={{ padding: 48, textAlign: 'center', fontSize: 12.5, color: 'var(--text-muted)', background: 'var(--surface)', minHeight: 320 }}>
                  {loadError
                    ? `The preview could not be loaded (${loadError}). The colours can still be set on the right.`
                    : 'Loading the preview…'}
                </div>}
          </div>
          <div style={{ fontSize: 11.5, color: 'var(--text-muted)', marginTop: 8, lineHeight: 1.45 }}>
            Point at a colour on the right to see what it paints. Click anything in the preview to find its colour.
          </div>
          {older && (
            <div style={{ fontSize: 11.5, color: 'var(--text-muted)', marginTop: 4, lineHeight: 1.45 }} data-brand-preview-older="">
              These previews come from an earlier version of the course player.
            </div>
          )}
        </div>

        <aside ref={panelRef} aria-label="Colours in this view"
          style={{ position: 'sticky', top: 12, maxHeight: 'calc(100vh - 24px)', overflow: 'auto', paddingRight: 2 }}>
          <div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 2 }}>
            {entryName}{views.length > 1 && view ? ' · ' + view.label : ''}
          </div>
          <div style={{ fontSize: 11.5, color: 'var(--text-muted)', marginBottom: 12 }}>
            {count} {count === 1 ? 'colour' : 'colours'} in this view
          </div>
          {cards.length === 0 && (
            <div style={{ fontSize: 12, color: 'var(--text-muted)', padding: '10px 12px', border: '1px dashed var(--border)', borderRadius: 'var(--radius-md)' }}>
              No colours in this view.
            </div>
          )}
          {cards.map((c, i) => {
            const opened = isOpen(c.title, i);
            const keys = c.rows.map(r => r.key);
            return (
              <div key={c.title} data-card={c.title} className={'brand-card' + (opened ? ' open' : '')}
                style={{ marginBottom: 8, border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', background: 'var(--surface)', overflow: 'hidden' }}>
                <button type="button" aria-expanded={opened} onClick={() => setCardOpen(c.title, !opened)}
                  onMouseEnter={() => point(keys, true)} onMouseLeave={() => point(keys, false)}
                  style={{ all: 'unset', boxSizing: 'border-box', display: 'flex', alignItems: 'center', gap: 8, width: '100%',
                    padding: '9px 10px', cursor: 'pointer', fontSize: 12.5, fontWeight: 600, color: 'var(--text)', fontFamily: 'var(--font-sans)' }}>
                  <span style={{ color: 'var(--text-muted)', flexShrink: 0, display: 'inline-flex', transition: 'transform .15s',
                    transform: opened ? 'rotate(90deg)' : 'none' }}>{CHEV}</span>
                  <span style={{ flex: 1, minWidth: 0 }}>{c.title}</span>
                  <span aria-hidden="true" style={{ display: 'inline-flex', gap: 3, flexShrink: 0 }}>
                    {c.rows.map(r => (
                      <span key={r.key} style={{ width: 12, height: 12, borderRadius: 3, border: '1px solid var(--border-strong)',
                        display: 'inline-block', background: valueOf(r) || 'transparent' }} />
                    ))}
                  </span>
                </button>
                {opened && (
                  <div style={{ padding: '2px 8px 8px', background: 'var(--surface-inset)', borderTop: '1px solid var(--border-faint)' }}>
                    {c.rows.map(r => (
                      <div key={r.key} data-row-key={r.key}
                        onMouseEnter={() => point([r.key], true, r)} onMouseLeave={() => point([r.key], false, r)}
                        style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', margin: '6px 0 0',
                          background: 'var(--surface)', borderRadius: 'var(--radius-md)',
                          border: '1px solid ' + (flash.indexOf(r.key) !== -1 ? 'var(--accent)' : 'var(--border)'),
                          boxShadow: flash.indexOf(r.key) !== -1 ? '0 0 0 3px var(--accent-bg)' : 'none' }}>
                        <div style={{ flex: 1, fontSize: 12, lineHeight: 1.3 }}>
                          {r.label}
                          {r.sub && <div style={{ fontSize: 10.5, color: 'var(--text-faint)', marginTop: 2, lineHeight: 1.35 }}>{r.sub}</div>}
                        </div>
                        <HeaderSwatch value={valueOf(r)} title={r.label} palette={brand.colours || []}
                          onChange={disabled ? undefined : v => (r.courseKey ? onCourseWide(r.courseKey, v) : onLayoutColour(entryType, r.path, v))} />
                      </div>
                    ))}
                  </div>
                )}
              </div>
            );
          })}
          <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
            <button type="button" className="btn sm" disabled={disabled}
              onClick={() => (isCourse ? onResetCourseWide() : onResetLayout(entryType))}>
              {isCourse ? 'Reset course-wide colours' : 'Reset this layout'}
            </button>
          </div>
        </aside>
      </div>
    </section>
  );
}

Object.assign(window, { BrandLayoutColours, loadBrandPreview });
