// Settings > Brand — the workspace's palette, and the colour combination every
// layout type starts from.
//
// ⚠️ NOT `surface-brand.jsx`, which despite its name is the COURSE SETTINGS
// screen (Surface 7). That file predates this one and renaming it would touch
// `app.jsx`, `index.html` and every reference to `window.SurfaceBrand`; the two
// notes at the top of each file are cheaper than a rename and say which is which.
//
// ── What Omar asked for, 2026-08-28 ─────────────────────────────────────────
// "By default, all Layouts for any client will start with these colors: Primary
//  #000000, #FFFFFF, #f8f8f8 and for Accent #2d6396, #97c3cb. Do not make any
//  Primary or Accent Mandatory. Client can add up to five colors for each. The
//  client can then select each layout to visualise how it looks like with the
//  current color combination. As soon as the layout is presented on the page, on
//  the right hand side, there will be a panel that will show all the colors used
//  for the selected layout … do not reinvent new code as this has been already
//  used in the Course Architecture when editing each layout."
//
// So nothing here is a new colour control. `ColorField` and `HeaderSwatch` are
// the same widgets every layout editor uses, and `PlayerPreview` is the same
// preview the Course Editor shows.
//
// ⚠️ CORRECTED 2026-08-29. This paragraph used to say the preview content was
// the demo samples — "the Latin placeholder text and grey artwork the mockup
// drew". That was wrong on every count, and the comment asserting it is
// precisely why nobody looked: those samples are the MOHG reference course, so
// the preview showed that course's English copy, that course's colours, and
// five media paths into that course's own files, which drew broken images.
// Omar found all three in one sitting. The preview now draws
// `layoutContentBase(type, 'blank')` — the skeleton a new screen is really born
// from, filled with the shared Lorem ipsum and `placeholder:` art.
// (`feedback_a_comment_is_not_a_contract`.)
//
// The one new thing is `BRAND_COLOUR_FIELDS`: which colours a layout has, what
// each paints, and the heading it belongs under. That did not exist anywhere —
// each editor hard-codes its own swatch labels — so it is generated from
// @dynamo/schema, where a test holds it to the real layout schemas both ways.
//
// ── WHAT SAVING DOES, and the mockup was wrong about it ─────────────────────
// Omar's decision, 2026-08-28: **new layouts only**. A screen added from now on
// is born in these colours; courses that already exist keep what they have. The
// mockup's own paragraph said the opposite — "updates the courses that inherit
// this brand, including ones already exported" — and printing that would be a
// false claim on the screen (`feedback_honest_gates_over_standins`). Asking the
// client whether to repaint existing courses is a later, separate phase.

const BRAND_GATEWAY_BASE = (window.DYNAMO_ENV || {}).gatewayBase;

// Read off the generated classic script (index.html loads it before this file),
// defended with an empty shape so a missing script degrades to a screen that
// says it cannot load rather than throwing and blanking the whole app
// (`feedback_the_no_build_frontend_fails_silently`).
const BRAND_FIELDS = (typeof BRAND_COLOUR_FIELDS !== 'undefined' && BRAND_COLOUR_FIELDS) || {};
const BRAND_TYPES = (typeof BRAND_LAYOUT_TYPES !== 'undefined' && BRAND_LAYOUT_TYPES) || [];
const FACTORY_PALETTE = (typeof FACTORY_BRAND_PALETTE !== 'undefined' && FACTORY_BRAND_PALETTE)
  || { primary: [], accent: [] };

const BRAND_MAX_COLOURS = 5;

/** Human name for a layout type, from the one list the whole app already uses. */
function brandLayoutLabel(type) {
  const meta = (window.LAYOUT_TYPES || []).find(t => t.id === type);
  return (meta && meta.label) || type;
}

// `initialLayout` exists ONLY so a render harness can ask for a specific layout
// without driving the dropdown. Nothing in the app passes it.
function SurfaceBrandSettings({ initialLayout } = {}) {
  const canWrite = typeof window.dynamoCan === 'function' && window.dynamoCan('brand:write');

  const [brand, setBrand] = React.useState(null);
  const [source, setSource] = React.useState(null);   // 'factory' | 'stored'
  const [loadError, setLoadError] = React.useState(null);
  const [selected, setSelected] = React.useState(
    (initialLayout && BRAND_TYPES.indexOf(initialLayout) !== -1 && initialLayout)
    || BRAND_TYPES[0] || 'title');
  const [applyNote, setApplyNote] = React.useState(null);

  // ── Load ─────────────────────────────────────────────────────────────────
  React.useEffect(() => {
    let alive = true;
    (async () => {
      try {
        if (typeof window.dynamoGetAccessToken !== 'function') {
          throw new Error('not signed in');
        }
        const token = await window.dynamoGetAccessToken();
        const res = await fetch(`${BRAND_GATEWAY_BASE}/v1/org/brand`, {
          headers: { authorization: `Bearer ${token}` },
        });
        if (!res.ok) throw new Error(`the server refused (${res.status})`);
        const body = await res.json();
        if (!alive) return;
        setBrand(body.brand);
        setSource(body.source);
      } catch (err) {
        if (!alive) return;
        // Show the factory palette rather than an empty screen, and SAY the
        // colours could not be loaded — a blank palette would read as "this
        // workspace has no colours", which is a different and wrong statement
        // (`feedback_absence_and_emptiness_read_the_same`).
        setBrand({ palette: FACTORY_PALETTE, layouts: {} });
        setSource(null);
        setLoadError((err && err.message) || 'the colours could not be loaded');
      }
    })();
    return () => { alive = false; };
  }, []);

  const save = React.useCallback(async () => {
    try {
      const token = await window.dynamoGetAccessToken();
      // ★ SAVE WHAT WAS SHOWN, not what happened to be stored.
      //
      // The panel and the preview above fall back to the palette for a layout
      // the client has never tuned. Sending `brand` unchanged would store an
      // empty `layouts` while the screen displayed a full set of colours — the
      // classic "what you saw is not what you saved". Resolving every type here
      // makes the two the same object. Types already tuned keep their stored
      // values; this only fills in the ones that were being inferred.
      const body = (typeof window.effectiveBrandLayouts === 'function')
        ? { ...brand, layouts: window.effectiveBrandLayouts(brand, BRAND_FIELDS) }
        : brand;
      const res = await fetch(`${BRAND_GATEWAY_BASE}/v1/org/brand`, {
        method: 'PUT',
        headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
        body: JSON.stringify(body),
      });
      if (res.ok) {
        setSource('stored');
        setBrand(body);
        // ★ AND TELL THE REST OF THE APP, or the save changes nothing a client
        // can see until they reload.
        //
        // `window.dynamoBrand` is filled by a fetch that runs once per sign-in.
        // Without this line a client could change their colours, press Apply,
        // press Save, add a screen — and get the colours the page started with.
        // That is the defect Omar reported on 2026-08-29 and the one that
        // mattered most, because it makes the whole feature look inert.
        if (typeof window.dynamoSetBrand === 'function') window.dynamoSetBrand(body);
        return { ok: true };
      }
      let detail = `the server refused (${res.status})`;
      try {
        const b = await res.json();
        if (b && (b.message || b.error)) detail = b.message || b.error;
      } catch { /* non-JSON refusal — the status is the whole story */ }
      return { ok: false, message: detail };
    } catch (err) {
      return { ok: false, message: (err && err.message) || 'the colours could not be sent' };
    }
  }, [brand]);

  if (!brand) {
    return <div style={{ padding: 24, fontSize: 13, color: 'var(--text-muted)' }}>
      Loading your colours…
    </div>;
  }

  const palette = brand.palette || { primary: [], accent: [] };
  const setPalette = (kind, list) =>
    setBrand(b => ({ ...b, palette: { ...b.palette, [kind]: list } }));

  const fields = BRAND_FIELDS[selected] || [];

  // ★ What this layout's colours ARE, which is not the same as what is stored.
  //
  // Stored values win; when the client has not tuned this type, the PALETTE
  // answers — the same spread "Apply to the layouts" would produce. Before
  // this, selecting a layout you had never touched showed an empty panel and a
  // preview in the demo course's colours, so the agreed palette was invisible
  // until you pressed Apply. Omar: "let's make sure that those colors are
  // already applied when the user select one of the layout".
  //
  // The SAME function answers for a newly created layout
  // (`window.brandColoursFor`), so the screen cannot promise one thing and the
  // Course Editor deliver another.
  const layoutColours = (typeof window.effectiveBrandColours === 'function')
    ? (window.effectiveBrandColours(brand, selected, BRAND_FIELDS) || {})
    : ((brand.layouts || {})[selected] || {});

  // The layout the preview draws.
  //
  // ★ `'blank'` — the SKELETON a new screen is actually born from, not the demo
  // course. Three of Omar's four reports on 2026-08-29 were the same root cause:
  // this used `LAYOUT_CONTENT_SAMPLES`, which is the MOHG reference course.
  //
  //   · its text is that course's English copy, not placeholder text
  //     ("Use only latin text as placeholder");
  //   · its colours are that course's own ("it shows colors picked from
  //     different courses");
  //   · and five of its media values are paths into that course's files —
  //     `content/silence.mp4`, `content/m2-03/02.mp4` and three more — which do
  //     not exist on the site, so the browser drew a broken image
  //     ("there are some in which there are missing image/video").
  //
  // `layoutContentBase(type, 'blank')` answers all three at once, and it is
  // existing code, not a second opinion: it returns the skeleton and runs
  // `fillPlaceholders` over it, which writes the Lorem ipsum in
  // `PLACEHOLDER_TEXT` and `placeholder:` media that resolves to real artwork
  // under `/placeholders/`. It is also more honest than the samples ever were —
  // the Brand screen's whole promise is "this is what a NEW screen looks like",
  // and this is literally the object a new screen starts as.
  const previewLayout = (() => {
    if (typeof window.layoutContentBase !== 'function') return null;
    const base = window.layoutContentBase(selected, 'blank');
    if (!base || !Object.keys(base).length) return null;
    const withType = { ...base, type: selected };
    return typeof window.applyBrandToLayout === 'function'
      ? window.applyBrandToLayout(withType, layoutColours)
      : withType;
  })();

  // ── "Apply to the layouts" — re-spread the palette over every layout ─────
  const applyToAll = () => {
    const spread = typeof window.spreadPaletteOverLayouts === 'function'
      ? window.spreadPaletteOverLayouts(palette, BRAND_FIELDS)
      : null;
    if (!spread) {
      // Refuses out loud rather than writing black over everything. A client
      // who has deleted every colour has nothing to apply.
      setApplyNote({ tone: 'warn',
        text: 'Add at least one colour above first — there is nothing to apply yet.' });
      return;
    }
    setBrand(b => ({ ...b, layouts: spread }));
    setApplyNote({ tone: 'ok',
      text: `Every layout now starts from your colours. Check each one below, `
        + `adjust anything you want, then Save.` });
  };

  const setLayoutColour = (path, value) =>
    setBrand(b => ({
      ...b,
      layouts: { ...(b.layouts || {}),
        [selected]: { ...((b.layouts || {})[selected] || {}), [path]: value } },
    }));

  // Who can save: admins and editors (Omar, 2026-08-28). The message names the
  // roles that CAN rather than the one the reader is, because the reader cannot
  // see their own role from this screen and "ask an admin" is the next step
  // either way.
  const blockedReason = canWrite ? null
    : 'Only admins and editors can change the workspace brand. You can look, but not save.';

  return (
    <div>
      <window.SurfaceHeader title="Brand"
        description={<>Your colours, and the combination every new screen starts
          from. Change a screen's colours individually in <strong>Course
          architecture</strong> — anything you set there is never overwritten
          from here.</>}>
        <window.DraftSaveButton
          dirtySignal={JSON.stringify(brand)}
          save={save}
          blockedReason={blockedReason}
          title="Write these colours to the server, so every new screen in this workspace starts from them" />
      </window.SurfaceHeader>

      <div style={{ padding: '18px 0 40px', display: 'flex', flexDirection: 'column', gap: 16 }}>

        {loadError && (
          <div style={{ display: 'flex', gap: 8, padding: '10px 12px',
            background: 'var(--warning-bg)', color: 'var(--warning-text)',
            border: '1px solid var(--border-faint)', borderRadius: 'var(--radius-md)',
            fontSize: 12.5, lineHeight: 1.5 }}>
            <I.AlertTriangle size={14} style={{ flexShrink: 0, marginTop: 2 }} />
            <div>
              <strong>These are the starting colours, not yours.</strong>{' '}
              Your workspace's colours could not be loaded ({loadError}). Saving now
              would replace whatever is stored — reload the page first.
            </div>
          </div>
        )}

        {/* ── Company logo ──────────────────────────────────────────────── */}
        <section className="card" style={{ padding: '16px 18px' }}>
          <div style={{ fontSize: 11.5, fontWeight: 600, letterSpacing: '.04em',
            textTransform: 'uppercase', color: 'var(--text-muted)' }}>Company logo</div>
          <div style={{ fontSize: 12.5, color: 'var(--text-muted)', marginTop: 4,
            lineHeight: 1.5 }}>
            Shown at the top of every course and on its home screen, in place of the
            one the course player ships with. PNG, JPG or WebP, up to 5&nbsp;MB —
            SVG is not accepted yet. An organisation that has its own logo still
            uses that one.
          </div>
          <div style={{ marginTop: 12 }}>
            <LogoBox value={brand.logo} disabled={!canWrite}
              onChange={ref => setBrand(b => {
                if (ref) return { ...b, logo: ref };
                const { logo, ...rest } = b;   // drop the key entirely
                return rest;
              })} />
          </div>
        </section>

        {/* ── The palette ───────────────────────────────────────────────── */}
        <section className="card" style={{ padding: '16px 18px' }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
            <div style={{ fontSize: 11.5, fontWeight: 600, letterSpacing: '.04em',
              textTransform: 'uppercase', color: 'var(--text-muted)' }}>Colours</div>
            {/* FIVE of each. The mockup's caption said "five primaries, plus up
                to three accents"; Omar's instruction, written after it, says
                five for both. His words win and the caption is corrected. */}
            <div style={{ fontSize: 12.5, color: 'var(--text-muted)', flex: 1, minWidth: 260 }}>
              Up to five of each, and none of them is required — remove any you
              do not use. Every layout can still be coloured on its own below.
            </div>
            {source === 'factory' && (
              <span className="pill" style={{ fontSize: 10.5 }}
                title="Nobody has chosen these yet — they are the colours every workspace starts with">
                starting colours
              </span>
            )}
          </div>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 14 }}>
            <PaletteRow
              name="Primary" hint="Layout backgrounds, panels, body text"
              colours={palette.primary || []} disabled={!canWrite}
              onChange={list => setPalette('primary', list)} />
            <PaletteRow
              name="Accents" hint="Hotspots, dots, markers, buttons"
              colours={palette.accent || []} disabled={!canWrite}
              onChange={list => setPalette('accent', list)} />
          </div>

          <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 16,
            flexWrap: 'wrap' }}>
            <button className="btn" onClick={applyToAll} disabled={!canWrite}
              title="Work out a colour combination for every layout from the colours above">
              Apply to the layouts
            </button>
            {applyNote && (
              <span style={{ fontSize: 12, lineHeight: 1.45, maxWidth: 520,
                color: applyNote.tone === 'warn' ? 'var(--warning-text)' : 'var(--text-muted)' }}>
                {applyNote.text}
              </span>
            )}
          </div>
        </section>

        {/* ── Pick a layout, see it, colour it ──────────────────────────── */}
        <section className="card" style={{ padding: '16px 18px' }}>
          <select className="field select-elegant" value={selected}
            onChange={e => { setSelected(e.target.value); setApplyNote(null); }}
            style={{ minWidth: 260, paddingRight: 26 }}>
            {BRAND_TYPES.map(t => (
              <option key={t} value={t}>{brandLayoutLabel(t)}</option>
            ))}
          </select>

          <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 280px',
            gap: 18, marginTop: 14, alignItems: 'start' }}>

            {/* The preview — the same renderer the Course Editor uses, on the
                same placeholder content. No real media, per the mockup. */}
            <div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius-md)',
              overflow: 'hidden', background: '#fff', minHeight: 320 }}>
              {previewLayout
                ? <window.PlayerPreview layout={previewLayout} />
                : <div style={{ padding: 48, textAlign: 'center', fontSize: 12.5,
                    color: 'var(--text-muted)' }}>
                    No preview for this layout yet. Its colours can still be set on the right.
                  </div>}
            </div>

            {/* The colour panel — one row per colour this layout actually has,
                under the heading it belongs to. */}
            <LayoutColourPanel
              fields={fields} values={layoutColours} disabled={!canWrite}
              onChange={setLayoutColour}
              layoutLabel={brandLayoutLabel(selected)} />
          </div>
        </section>

        {/* ── What changing this does ───────────────────────────────────── */}
        <div style={{ display: 'flex', gap: 10, padding: '12px 14px',
          background: 'var(--surface-inset)', border: '1px solid var(--border-faint)',
          borderRadius: 'var(--radius-md)', fontSize: 12.5, color: 'var(--text-muted)',
          lineHeight: 1.55 }}>
          <I.Info size={14} style={{ flexShrink: 0, marginTop: 2 }} />
          <div>
            <strong style={{ color: 'var(--text)' }}>Screens you add from now on start
            with these colours.</strong> Courses you have already made keep the colours
            they have — nothing you have built is repainted, and nothing you set on an
            individual screen is overwritten. These colours are also offered first in
            every colour picker across the app.
          </div>
        </div>
      </div>
    </div>
  );
}

// ── The logo box ───────────────────────────────────────────────────────────
//
// Three states, and the third is the one that is usually skipped: nothing
// chosen, a logo chosen, and a upload that FAILED. A failed upload that leaves
// the box looking empty is indistinguishable from never having tried
// (`feedback_a_failure_state_must_read_back_not_infer`), so the refusal is shown
// with what went wrong.
function LogoBox({ value, onChange, disabled }) {
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [src, setSrc] = React.useState(null);
  const inputRef = React.useRef(null);

  // `resolveAssetUrl` returns EITHER a url string (local bytes just uploaded, or
  // a cached signed url) OR a promise for one. Handling only the string would
  // show a broken image every time the page is reloaded, which is exactly when a
  // logo most needs to be visible. A broken-image icon is worse than the empty
  // placeholder: it says something is wrong when nothing is.
  React.useEffect(() => {
    let alive = true;
    if (!value || typeof window.resolveAssetUrl !== 'function') { setSrc(null); return; }
    const out = window.resolveAssetUrl(value);
    if (out && typeof out.then === 'function') {
      out.then(u => { if (alive) setSrc(u || null); }).catch(() => { if (alive) setSrc(null); });
    } else {
      setSrc(typeof out === 'string' ? out : null);
    }
    return () => { alive = false; };
  }, [value]);

  const pick = async (file) => {
    if (!file) return;
    setBusy(true); setError(null);
    try {
      if (typeof window.uploadWorkspaceLogo !== 'function') {
        throw new Error('uploading is not available on this page');
      }
      onChange(await window.uploadWorkspaceLogo(file));
    } catch (err) {
      setError((err && err.message) || 'the logo could not be uploaded');
    } finally {
      setBusy(false);
      // Let the same file be chosen again after a failure — without this a
      // retry of the identical file fires no change event at all.
      if (inputRef.current) inputRef.current.value = '';
    }
  };

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '12px 14px',
      background: 'var(--surface)', border: '1px solid var(--border)',
      borderRadius: 'var(--radius-md)', flexWrap: 'wrap' }}>
      <div style={{ width: 64, height: 40, flexShrink: 0, borderRadius: 4,
        background: 'var(--surface-inset)', border: '1px solid var(--border-faint)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        overflow: 'hidden' }}>
        {value && src
          ? <img alt="Your logo" src={src}
              style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
          : <I.Image size={16} style={{ color: 'var(--text-faint)' }} />}
      </div>
      <div style={{ flex: 1, minWidth: 180 }}>
        <div style={{ fontSize: 13, fontWeight: 500 }}>
          {value
            ? (typeof window.assetLabel === 'function' ? window.assetLabel(value) : 'Your logo')
            : 'No logo yet'}
        </div>
        <div style={{ fontSize: 11.5, lineHeight: 1.4, marginTop: 2,
          color: error ? 'var(--error-text)' : 'var(--text-muted)' }}>
          {error
            ? `Not uploaded: ${error}`
            : value
              ? 'Courses you build from now on will carry this.'
              : 'Courses carry the logo the course player ships with.'}
        </div>
      </div>
      <input ref={inputRef} type="file" accept="image/png,image/jpeg,image/webp"
        style={{ display: 'none' }}
        onChange={e => pick(e.target.files && e.target.files[0])} />
      <div style={{ display: 'flex', gap: 8 }}>
        <button className="btn sm" disabled={disabled || busy}
          onClick={() => inputRef.current && inputRef.current.click()}>
          {busy ? 'Uploading…' : value ? 'Replace' : 'Upload'}
        </button>
        {value && (
          <button className="btn sm ghost" disabled={disabled || busy}
            onClick={() => { setError(null); onChange(null); }}>Remove</button>
        )}
      </div>
    </div>
  );
}

// ── One palette row: a list of colours, each removable, up to five ──────────
//
// NOTHING IS MANDATORY — Omar, explicitly. Black and white carry the same `×`
// as every other entry. An earlier draft pinned them as undeletable and did not
// count them against the allowance; that rule is gone, and a control that
// refuses to do the obvious thing without saying why is worse than one that
// simply does it (`feedback_a_greyed_control_is_not_a_missing_capability`).
function PaletteRow({ name, hint, colours, onChange, disabled }) {
  const full = colours.length >= BRAND_MAX_COLOURS;
  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, padding: '10px 12px',
      background: 'var(--surface)', border: '1px solid var(--border)',
      borderRadius: 'var(--radius-md)', flexWrap: 'wrap' }}>
      <div style={{ width: 150, flexShrink: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 600 }}>{name}</div>
        <div style={{ fontSize: 11.5, color: 'var(--text-muted)', lineHeight: 1.35,
          marginTop: 2 }}>{hint}</div>
      </div>
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', flex: 1, minWidth: 240 }}>
        {colours.map((c, i) => (
          <div key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}>
            {/* The SAME control every layout editor uses. */}
            <ColorField value={c} palette={colours}
              onChange={disabled ? undefined
                : v => onChange(colours.map((x, j) => (j === i ? v : x)))} />
            <button className="btn sm ghost" disabled={disabled}
              title={`Remove ${c}`}
              onClick={() => onChange(colours.filter((_, j) => j !== i))}
              style={{ padding: '0 6px', minWidth: 0 }}>×</button>
          </div>
        ))}
        {colours.length === 0 && (
          // An empty list is a CHOICE, and the screen says which choice it is.
          <span style={{ fontSize: 12, color: 'var(--text-faint)', fontStyle: 'italic',
            alignSelf: 'center' }}>
            None. Nothing from this group will be offered in the colour pickers.
          </span>
        )}
        {!full && (
          <button className="btn sm" disabled={disabled}
            onClick={() => onChange([...colours, '#000000'])}
            style={{ borderStyle: 'dashed' }}>
            + Add colour
          </button>
        )}
      </div>
    </div>
  );
}

// ── The right-hand panel: every colour the selected layout uses ─────────────
//
// Grouped exactly as the mockup draws it — "Title block", "Tabs", "Progress
// gate" — and each row named after what the learner sees rather than after the
// field. The groups and labels come from the generated registry, so this
// component holds no opinion about any particular layout.
function LayoutColourPanel({ fields, values, onChange, disabled, layoutLabel }) {
  if (!fields.length) {
    return (
      <div style={{ fontSize: 12.5, color: 'var(--text-muted)', padding: '10px 12px',
        border: '1px solid var(--border)', borderRadius: 'var(--radius-md)' }}>
        This layout has no colours of its own.
      </div>
    );
  }
  const groups = [];
  fields.forEach(f => {
    const last = groups[groups.length - 1];
    if (last && last.name === f.group) last.rows.push(f);
    else groups.push({ name: f.group, rows: [f] });
  });

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {groups.map(g => (
        <div key={g.name}>
          <div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 6 }}>{g.name}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {g.rows.map(f => (
              <div key={f.path}>
                {/* The SAME row the layout editors use for a free-standing colour. */}
                <InlineColorRow label={f.label} value={values[f.path]}
                  onChange={disabled ? undefined : v => onChange(f.path, v)} />
                {(f.perItem || f.note) && (
                  <div style={{ fontSize: 10.5, color: 'var(--text-faint)', lineHeight: 1.4,
                    margin: '3px 2px 0' }}>
                    {f.perItem ? 'Applies to every one of these on the screen. ' : ''}
                    {f.note || ''}
                  </div>
                )}
              </div>
            ))}
          </div>
        </div>
      ))}
      <div style={{ fontSize: 11, color: 'var(--text-faint)', lineHeight: 1.45 }}>
        These are the colours a new <strong>{layoutLabel}</strong> screen will start
        with. Save at the top of the page to keep them.
      </div>
    </div>
  );
}

Object.assign(window, { SurfaceBrandSettings });
